From 2f2e38cce0874bcb7f0cb1984fde27c2ad288624 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 22 Aug 2026 06:16:37 +0000 Subject: [PATCH 1/4] Add Draw.io diagram for AI Gateway and MCP architecture Three-page diagram mapping the whiteboard design to am-mcp-gateway, documenting chat SSE flow and LLM/MCP proxy paths used by am-agents. Co-authored-by: Munish Kumar --- docs/diagrams/02-ai-gateway-mcp-design.drawio | 431 ++++++++++++++++++ 1 file changed, 431 insertions(+) create mode 100644 docs/diagrams/02-ai-gateway-mcp-design.drawio diff --git a/docs/diagrams/02-ai-gateway-mcp-design.drawio b/docs/diagrams/02-ai-gateway-mcp-design.drawio new file mode 100644 index 0000000..0e46c53 --- /dev/null +++ b/docs/diagrams/02-ai-gateway-mcp-design.drawio @@ -0,0 +1,431 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 9e709d83366a127d176076662a138a16b6a9450a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 22 Aug 2026 06:16:41 +0000 Subject: [PATCH 2/4] Document new AI Gateway diagram in diagrams README Co-authored-by: Munish Kumar --- docs/diagrams/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/diagrams/README.md b/docs/diagrams/README.md index 8e39bfb..d64cfbc 100644 --- a/docs/diagrams/README.md +++ b/docs/diagrams/README.md @@ -9,6 +9,7 @@ Enterprise / portal surfaces only. |------|---------| | [enterprise-agent-ecosystem.drawio](enterprise-agent-ecosystem.drawio) | Multi-page enterprise master | | [01-three-surfaces-overview.drawio](01-three-surfaces-overview.drawio) | Three surfaces overview | +| [02-ai-gateway-mcp-design.drawio](02-ai-gateway-mcp-design.drawio) | AI Gateway + MCP design — whiteboard mapping, chat flow, LLM/MCP proxy | ## How to open From 27531158f59e8e4b060389c6d702c74324c5d1a6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 22 Aug 2026 06:19:15 +0000 Subject: [PATCH 3/4] Fix Draw.io diagram rendering with clear target-state flows - Regenerate diagram with proper XML escaping ( for line breaks) - Use 1200px viewport so content is visible without zoom - Page 1: vertical layer stack with gateway internal pipeline - Page 2: numbered chat request sequence - Page 3: LLM, MCP, and security paths - Add generator script for future updates Co-authored-by: Munish Kumar --- docs/diagrams/02-ai-gateway-mcp-design.drawio | 526 +++++++++--------- docs/diagrams/generate_ai_gateway_diagram.py | 357 ++++++++++++ 2 files changed, 609 insertions(+), 274 deletions(-) create mode 100644 docs/diagrams/generate_ai_gateway_diagram.py diff --git a/docs/diagrams/02-ai-gateway-mcp-design.drawio b/docs/diagrams/02-ai-gateway-mcp-design.drawio index 0e46c53..6b8214b 100644 --- a/docs/diagrams/02-ai-gateway-mcp-design.drawio +++ b/docs/diagrams/02-ai-gateway-mcp-design.drawio @@ -1,429 +1,407 @@ - - - + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + - + - + - - - - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + - + - + - + - + - + - + - + - + - - + + - - - - - - - - - - - - - - - - - - - + + - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + + + + - + - + - + - + - + - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + + + + - - + + - - + + - - - - - - - - + + - - + + diff --git a/docs/diagrams/generate_ai_gateway_diagram.py b/docs/diagrams/generate_ai_gateway_diagram.py new file mode 100644 index 0000000..a0adc10 --- /dev/null +++ b/docs/diagrams/generate_ai_gateway_diagram.py @@ -0,0 +1,357 @@ +#!/usr/bin/env python3 +"""Generate 02-ai-gateway-mcp-design.drawio with clear vertical/horizontal flows.""" + +from pathlib import Path + +OUT = Path(__file__).parent / "02-ai-gateway-mcp-design.drawio" + +STYLE = { + "title": "text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=20;fontStyle=1;fontColor=#0F172A;", + "lane": "swimlane;horizontal=1;startSize=30;fillColor={bg};strokeColor={stroke};fontColor={fg};fontStyle=1;rounded=1;", + "box_blue": "rounded=1;whiteSpace=wrap;html=1;fillColor=#0284C7;fontColor=#FFFFFF;strokeColor=#0369A1;fontSize=12;", + "box_indigo": "rounded=1;whiteSpace=wrap;html=1;fillColor=#4F46E5;fontColor=#FFFFFF;strokeColor=#312E81;fontSize=12;", + "box_purple": "rounded=1;whiteSpace=wrap;html=1;fillColor=#7C3AED;fontColor=#FFFFFF;strokeColor=#6D28D9;fontSize=12;", + "box_pink": "rounded=1;whiteSpace=wrap;html=1;fillColor=#DB2777;fontColor=#FFFFFF;strokeColor=#BE185D;fontSize=12;", + "box_teal": "rounded=1;whiteSpace=wrap;html=1;fillColor=#0D9488;fontColor=#FFFFFF;strokeColor=#0F766E;fontSize=12;", + "box_gray": "rounded=1;whiteSpace=wrap;html=1;fillColor=#64748B;fontColor=#FFFFFF;strokeColor=#475569;fontSize=12;", + "box_sky": "rounded=1;whiteSpace=wrap;html=1;fillColor=#0EA5E9;fontColor=#FFFFFF;strokeColor=#0284C7;fontSize=12;", + "box_orange": "rounded=1;whiteSpace=wrap;html=1;fillColor=#F59E0B;fontColor=#FFFFFF;strokeColor=#D97706;fontSize=12;", + "box_green": "rounded=1;whiteSpace=wrap;html=1;fillColor=#059669;fontColor=#FFFFFF;strokeColor=#047857;fontSize=12;", + "box_violet": "rounded=1;whiteSpace=wrap;html=1;fillColor=#6366F1;fontColor=#FFFFFF;strokeColor=#4F46E5;fontSize=12;", + "box_planned": "rounded=1;whiteSpace=wrap;html=1;fillColor=#EDE9FE;fontColor=#5B21B6;strokeColor=#7C3AED;dashed=1;dashPattern=8 4;fontSize=11;", + "note": "text;html=1;strokeColor=#CBD5E1;fillColor=#FFFFFF;align=left;verticalAlign=top;spacingLeft=10;spacingTop=8;fontColor=#0F172A;rounded=1;fontSize=11;", + "arrow": "edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#334155;strokeWidth=2;endArrow=classic;html=1;", + "arrow_blue": "edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#0284C7;strokeWidth=2;endArrow=classic;html=1;fontColor=#0369A1;", + "arrow_dash": "edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#6366F1;strokeWidth=2;endArrow=classic;dashed=1;html=1;fontColor=#4338CA;", + "arrow_down": "edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#334155;strokeWidth=3;endArrow=block;endFill=1;html=1;", +} + + +def esc(text: str) -> str: + return ( + text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace('"', """) + .replace("\n", " ") + ) + + +class Diagram: + def __init__(self, diagram_id: str, name: str, width: int = 1200, height: int = 900): + self.diagram_id = diagram_id + self.name = name + self.width = width + self.height = height + self.cells: list[str] = [] + self._n = 0 + + def uid(self, prefix: str) -> str: + self._n += 1 + return f"{prefix}{self._n}" + + def cell(self, cid: str, value: str, style: str, x: int, y: int, w: int, h: int, parent: str = "1") -> str: + self.cells.append( + f' \n' + f' \n' + f" " + ) + return cid + + def lane(self, cid: str, label: str, bg: str, stroke: str, fg: str, x: int, y: int, w: int, h: int) -> str: + style = STYLE["lane"].format(bg=bg, stroke=stroke, fg=fg) + return self.cell(cid, label, style, x, y, w, h) + + def edge(self, eid: str, source: str, target: str, label: str = "", style: str | None = None, parent: str = "1") -> None: + style = style or STYLE["arrow"] + label_attr = f' value="{esc(label)}"' if label else "" + self.cells.append( + f' \n' + f' \n' + f" " + ) + + def edge_points(self, eid: str, source: str, target: str, points: list[tuple[int, int]], label: str = "", style: str | None = None) -> None: + style = style or STYLE["arrow"] + label_attr = f' value="{esc(label)}"' if label else "" + pts = "\n".join(f' ' for x, y in points) + self.cells.append( + f' \n' + f" \n" + f" \n{pts}\n" + f" \n" + f" \n" + f" " + ) + + def render(self) -> str: + body = "\n".join(self.cells) + return f""" + + + + +{body} + + + """ + + +def build_target_architecture() -> Diagram: + d = Diagram("target-architecture", "1 - Target Architecture", 1200, 1300) + + d.cell("p1_title", "AI Gateway + MCP — Target Architecture", STYLE["title"], 150, 20, 900, 40) + d.cell( + "p1_sub", + "Whiteboard vision implemented via am-platform/am-mcp-gateway (L2) routing am-agents (L3) to MCP core services (L4)", + "text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=12;fontColor=#64748B;", + 80, + 55, + 1040, + 30, + ) + + # L1 + l1 = d.lane("p1_l1", "L1 — Client Surfaces (UI · IDE · Mobile)", "#EEF2FF", "#4F46E5", "#312E81", 40, 100, 1120, 110) + ui = d.cell("p1_ui", "Web UI\nam-modern-ui\nAI Chat · Widgets", STYLE["box_indigo"], 60, 45, 180, 55, l1) + ide = d.cell("p1_ide", "IDE\nCursor / VS Code\nMCP tools", STYLE["box_indigo"], 270, 45, 160, 55, l1) + mob = d.cell("p1_mob", "Mobile\n(future)\nSame API", STYLE["box_planned"], 460, 45, 130, 55, l1) + pb = d.cell("p1_pb", "Portal B\nai-bots React\nWorkspace", STYLE["box_green"], 620, 45, 160, 55, l1) + pc = d.cell("p1_pc", "Portal C\nkagent UI\nSRE ops", "rounded=1;whiteSpace=wrap;html=1;fillColor=#EA580C;fontColor=#FFFFFF;strokeColor=#C2410C;fontSize=12;", 810, 45, 140, 55, l1) + + # Auth strip + lauth = d.lane("p1_auth", "Auth & Security — Keycloak JWT · RBAC · Guardrails", "#F1F5F9", "#64748B", "#0F172A", 40, 230, 1120, 80) + kc = d.cell("p1_kc", "Keycloak\nJWT + roles", STYLE["box_gray"], 420, 40, 140, 35, lauth) + rbac = d.cell("p1_rbac", "RBAC\ntenant · agent allowlist", STYLE["box_gray"], 590, 40, 170, 35, lauth) + + # L2 Gateway - internal flow left to right + l2 = d.lane("p1_l2", "L2 — AI Gateway am-mcp-gateway :8120 (am-platform repo)", "#F0F9FF", "#0284C7", "#0C4A6E", 40, 330, 1120, 160) + chat = d.cell("p1_chat", "POST\n/api/v1/chat/stream", STYLE["box_blue"], 40, 45, 130, 50, l2) + preg = d.cell("p1_preg", "Prompt Registry\nagents.yaml", STYLE["box_sky"], 190, 45, 130, 50, l2) + route = d.cell("p1_route", "Intent Router\nkeywords + LLM", STYLE["box_sky"], 340, 45, 130, 50, l2) + sec = d.cell("p1_sec", "AI Security\ninput/output guard", STYLE["box_sky"], 490, 45, 130, 50, l2) + sse = d.cell("p1_sse", "Unified SSE\nnormalize events", STYLE["box_sky"], 640, 45, 130, 50, l2) + llm = d.cell("p1_llm", "LLM Proxy\n/agent/llm/completions", STYLE["box_sky"], 790, 45, 150, 50, l2) + mcp = d.cell("p1_mcp", "MCP Proxy\ntool routes", STYLE["box_sky"], 960, 45, 130, 50, l2) + d.edge("p1_e_l2_1", chat, preg, parent=l2) + d.edge("p1_e_l2_2", preg, route, parent=l2) + d.edge("p1_e_l2_3", route, sec, parent=l2) + d.edge("p1_e_l2_4", sec, sse, parent=l2) + d.edge("p1_e_l2_5", sse, llm, parent=l2) + d.edge("p1_e_l2_6", llm, mcp, parent=l2) + + # L3 agents + l3 = d.lane("p1_l3", "L3 — Domain Agents (am-agents repo) — gateway routes by intent", "#F5F3FF", "#7C3AED", "#4C1D95", 40, 510, 1120, 110) + fin = d.cell("p1_fin", "fin-agent :8100\nPortfolio · widgets", STYLE["box_pink"], 60, 45, 150, 55, l3) + test = d.cell("p1_test", "ui-test-agent :8130\nPlaywright QA", STYLE["box_purple"], 240, 45, 150, 55, l3) + tool = d.cell("p1_tool", "tool-agent :8141\nVault · K8s · Mongo", STYLE["box_purple"], 420, 45, 150, 55, l3) + dev = d.cell("p1_dev", "ai-bots dev :5000\nCode · Jira · RAG", STYLE["box_green"], 600, 45, 150, 55, l3) + scribe = d.cell("p1_scribe", "scribe-agent\n(planned)", STYLE["box_planned"], 780, 45, 130, 55, l3) + sup = d.cell("p1_sup", "support-agent\n(planned)", STYLE["box_planned"], 930, 45, 130, 55, l3) + + # L4 MCP + l4 = d.lane("p1_l4", "L4 — MCP Core Services (Whiteboard: Portfolio · Trade · Market · Stock Analysis)", "#F0FDFA", "#0D9488", "#134E4A", 40, 640, 1120, 110) + port = d.cell("p1_port", "Portfolio SDK\nholdings · PnL", STYLE["box_teal"], 60, 45, 130, 55, l4) + trade = d.cell("p1_trade", "Trade\norders", STYLE["box_teal"], 210, 45, 100, 55, l4) + market = d.cell("p1_market", "Market Analysis\nETF · MF", STYLE["box_teal"], 330, 45, 130, 55, l4) + stock = d.cell("p1_stock", "Stock Analysis", STYLE["box_teal"], 480, 45, 120, 55, l4) + vault = d.cell("p1_vault", "Vault MCP\nsecrets", STYLE["box_teal"], 620, 45, 110, 55, l4) + k8s = d.cell("p1_k8s", "K8s MCP\ndeploy", STYLE["box_teal"], 750, 45, 100, 55, l4) + dbmcp = d.cell("p1_db", "DB MCP Toolbox\nPostgres · Mongo", STYLE["box_teal"], 870, 45, 150, 55, l4) + + # Platform + lplat = d.lane("p1_plat", "Platform — LiteLLM · Langfuse · Vault · Usage/Billing", "#EEF2FF", "#6366F1", "#312E81", 40, 770, 1120, 90) + lit = d.cell("p1_lit", "LiteLLM\nmodel routing", STYLE["box_violet"], 200, 40, 140, 45, lplat) + lf = d.cell("p1_lf", "Langfuse\ntraces · cost", STYLE["box_violet"], 380, 40, 140, 45, lplat) + hv = d.cell("p1_hv", "Vault\nsecrets", STYLE["box_violet"], 560, 40, 120, 45, lplat) + + # Vertical flow arrows between layers + d.edge("p1_v1", ui, kc, "all clients", STYLE["arrow_down"]) + d.edge("p1_v2", kc, chat, "authenticated", STYLE["arrow_down"]) + d.edge("p1_v3", route, fin, "route: finance", STYLE["arrow_down"]) + d.edge("p1_v4", route, test, "route: test", STYLE["arrow_down"]) + d.edge("p1_v5", route, tool, "route: devops", STYLE["arrow_down"]) + d.edge("p1_v6", fin, port, "MCP tools", STYLE["arrow_down"]) + d.edge("p1_v7", tool, vault, "MCP tools", STYLE["arrow_down"]) + d.edge("p1_v8", llm, lit, "LLM calls", STYLE["arrow_dash"]) + d.edge("p1_v9", lit, lf, "trace", STYLE["arrow_dash"]) + + d.cell( + "p1_legend", + "FLOW: Clients -> Auth -> am-mcp-gateway (route) -> Domain Agent -> MCP Service -> LiteLLM/Langfuse\n" + "TODAY: fin-agent, ui-test-agent, tool-agent live. PLANNED: agents.yaml registry, scribe, unified SSE schema.", + STYLE["note"], + 40, + 880, + 1120, + 55, + ) + d.cell( + "p1_map", + "Whiteboard mapping:\n" + "AI Gateway = am-mcp-gateway | fin-agent = am-fin-agent | MCP server = tool-agent + fin MCP + LiteLLM registry | " + "Prompt Registry = agents.yaml | Modern UI = am-modern-ui", + "text;html=1;strokeColor=#F59E0B;fillColor=#FFFBEB;align=left;verticalAlign=top;spacingLeft=10;spacingTop=8;fontColor=#92400E;rounded=1;fontSize=11;", + 40, + 950, + 1120, + 70, + ) + return d + + +def build_chat_flow() -> Diagram: + d = Diagram("chat-flow", "2 - Chat Request Flow", 1200, 850) + + d.cell("p2_title", "Chat Request Flow — Example: Show my portfolio PnL", STYLE["title"], 200, 20, 800, 40) + + # Step pipeline + lane = d.lane("p2_lane", "6-Step Execution Pipeline", "#FFF7ED", "#F59E0B", "#92400E", 40, 70, 1120, 80) + steps = [] + labels = ["1 User msg", "2 Auth", "3 Route", "4 Agent", "5 MCP exec", "6 SSE back"] + x = 30 + for i, lbl in enumerate(labels): + cid = f"p2_s{i+1}" + steps.append(d.cell(cid, lbl, STYLE["box_orange"], x, 40, 110, 35, lane)) + x += 130 + for i in range(len(steps) - 1): + d.edge(f"p2_es{i+1}", steps[i], steps[i + 1], parent=lane) + + # Main horizontal sequence + user = d.cell("p2_user", "User", "shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fillColor=#4F46E5;strokeColor=#312E81;", 60, 200, 40, 70) + ui = d.cell("p2_ui", "L1 UI\nam-modern-ui", STYLE["box_indigo"], 140, 210, 120, 55) + gw = d.cell("p2_gw", "L2 Gateway\nam-mcp-gateway\n:8120", STYLE["box_blue"], 300, 200, 150, 70) + agent = d.cell("p2_agent", "L3 fin-agent\n:8100", STYLE["box_pink"], 490, 210, 120, 55) + mcp = d.cell("p2_mcp", "L4 Portfolio SDK", STYLE["box_teal"], 650, 210, 130, 55) + lit = d.cell("p2_lit", "LiteLLM", STYLE["box_violet"], 820, 210, 100, 55) + lf = d.cell("p2_lf", "Langfuse", STYLE["box_violet"], 960, 210, 100, 55) + + d.edge("p2_e1", user, ui, "1") + d.edge("p2_e2", ui, gw, "2 POST /chat/stream\n+ JWT", STYLE["arrow_blue"]) + d.edge("p2_e3", gw, agent, "3 route: finance\nagent_selected", STYLE["arrow_blue"]) + d.edge("p2_e4", agent, mcp, "4 tool_call:\nget_holdings") + d.edge("p2_e5", agent, lit, "5 LLM via\n/agent/llm/completions", STYLE["arrow_dash"]) + d.edge("p2_e6", lit, lf, "trace") + d.edge_points("p2_e7", agent, ui, [(540, 340), (200, 340)], "6 SSE: token, artifact(widget), done", STYLE["arrow_blue"]) + + d.cell( + "p2_sse", + "SSE Events (gateway normalizes all agents to one schema):\n" + "agent_selected | stage | tool_call | token | artifact | memory_update | done", + STYLE["note"], + 40, + 390, + 540, + 70, + ) + d.cell( + "p2_req", + "Gateway -> Agent request body:\n" + "message, sessionId, userId, traceId, agentContext { pinnedAgent, requirementIds }", + STYLE["note"], + 600, + 390, + 540, + 70, + ) + + d.cell( + "p2_modes", + "No-agent mode: gateway answers via LiteLLM directly (simple queries).\n" + "Multi-agent mode: router chains agents using agentContext.workingSummary.", + "text;html=1;strokeColor=#CBD5E1;fillColor=#F8FAFC;align=left;verticalAlign=top;spacingLeft=10;spacingTop=8;fontColor=#475569;rounded=1;fontSize=11;", + 40, + 480, + 1100, + 50, + ) + + # agents.yaml + d.cell( + "p2_yaml", + "agents.yaml registry (planned — replaces hardcoded routes):\n" + "finance -> am-fin-agent:8100 keywords: [portfolio, pnl, trade]\n" + "test -> am-ui-test-agent:8130 keywords: [playwright, ui, test]\n" + "devops -> am-tool-agent:8141 keywords: [k8s, vault, deploy]", + "text;html=1;strokeColor=#0284C7;fillColor=#F0F9FF;align=left;verticalAlign=top;spacingLeft=10;spacingTop=8;fontColor=#0C4A6E;rounded=1;fontSize=11;", + 40, + 550, + 1100, + 80, + ) + return d + + +def build_llm_mcp_flow() -> Diagram: + d = Diagram("llm-mcp-flow", "3 - LLM and MCP Paths", 1200, 900) + + d.cell("p3_title", "LLM & MCP Proxy Paths — How agents use the gateway today", STYLE["title"], 150, 20, 900, 40) + + # Path A + la = d.lane("p3_la", "Path A — Agent LLM calls (MCP_GATEWAY_BASE_URL)", "#EEF2FF", "#6366F1", "#312E81", 40, 70, 1120, 130) + agents = d.cell("p3_agents", "ui-test-agent\ntool-agent\ndb-agent\nfin-portfolio-agent", STYLE["box_purple"], 40, 45, 160, 70, la) + gw = d.cell("p3_gw", "am-mcp-gateway\nPOST /api/v1/agent/llm/completions", STYLE["box_blue"], 250, 45, 220, 70, la) + lit = d.cell("p3_lit", "LiteLLM\nOpenAI · Gemini · Together", STYLE["box_violet"], 520, 45, 180, 70, la) + lf = d.cell("p3_lf", "Langfuse\ntraces · sessionId · cost", STYLE["box_violet"], 750, 45, 160, 70, la) + env = d.cell("p3_env", "Env: MCP_GATEWAY_BASE_URL\nAuth: Keycloak client_credentials\nVault: LITELLM_MASTER_KEY", STYLE["note"], 940, 45, 150, 70, la) + d.edge("p3_a1", agents, gw, "HTTP POST", parent=la) + d.edge("p3_a2", gw, lit, "proxy", parent=la) + d.edge("p3_a3", lit, lf, "observability", parent=la) + + # Path B + lb = d.lane("p3_lb", "Path B — MCP tool registration & execution", "#F0FDFA", "#0D9488", "#134E4A", 40, 220, 1120, 150) + sync = d.cell("p3_sync", "sync_litellm_mcp_tools\n(am-mcp-gateway)", STYLE["box_teal"], 40, 45, 170, 55, lb) + reg = d.cell("p3_reg", "LiteLLM MCP Registry\nK8s ConfigMap", STYLE["box_violet"], 240, 45, 170, 55, lb) + gw2 = d.cell("p3_gw2", "am-mcp-gateway\nMCP proxy routes", STYLE["box_blue"], 440, 45, 170, 55, lb) + cursor = d.cell("p3_cursor", "Cursor / IDE\nMCP client", STYLE["box_indigo"], 640, 45, 130, 55, lb) + finm = d.cell("p3_finm", "fin MCP\nportfolio tools", STYLE["box_teal"], 40, 105, 120, 40, lb) + uim = d.cell("p3_uim", "ui-test MCP", STYLE["box_teal"], 180, 105, 110, 40, lb) + toolm = d.cell("p3_toolm", "tool-agent MCP\nvault · k8s", STYLE["box_teal"], 310, 105, 130, 40, lb) + dbm = d.cell("p3_dbm", "DB MCP Toolbox", STYLE["box_teal"], 460, 105, 130, 40, lb) + d.edge("p3_b1", sync, reg, "register", parent=lb) + d.edge("p3_b2", reg, gw2, "expose", parent=lb) + d.edge("p3_b3", cursor, gw2, "MCP protocol", parent=lb) + d.edge("p3_b4", finm, reg, parent=lb) + d.edge("p3_b5", uim, reg, parent=lb) + d.edge("p3_b6", toolm, reg, parent=lb) + + # Path C security + lc = d.lane("p3_lc", "Path C — Security loop (every request wraps LiteLLM)", "#F1F5F9", "#64748B", "#0F172A", 40, 390, 1120, 100) + boxes = [] + labels = ["Request", "JWT + RBAC", "SPT Guard", "Prompt filter", "LiteLLM", "Output guard", "Response"] + x = 30 + for lbl in labels: + style = STYLE["box_violet"] if lbl == "LiteLLM" else STYLE["box_gray"] + if lbl == "Response": + style = STYLE["box_blue"] + boxes.append(d.cell(f"p3_c_{lbl.replace(' ', '_')}", lbl, style, x, 45, 110, 45, lc)) + x += 130 + for i in range(len(boxes) - 1): + d.edge(f"p3_c_e{i+1}", boxes[i], boxes[i + 1], parent=lc) + + d.cell( + "p3_note", + "K8s service: am-mcp-gateway.am-apps-{env}.svc.cluster.local:8120\n" + "All agents in am-agents set MCP_GATEWAY_BASE_URL in Helm values (dev/preprod/prod)", + STYLE["note"], + 40, + 510, + 1100, + 50, + ) + return d + + +def main() -> None: + pages = [build_target_architecture(), build_chat_flow(), build_llm_mcp_flow()] + content = "\n".join(p.render() for p in pages) + xml = f""" +{content} + +""" + OUT.write_text(xml, encoding="utf-8") + print(f"Wrote {OUT} ({len(xml)} bytes, {len(pages)} pages)") + + +if __name__ == "__main__": + main() From 033947f9db03841aea422140ad38e1b133f56e6d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 22 Aug 2026 06:21:49 +0000 Subject: [PATCH 4/4] Fix blank Draw.io: match working repo format and add fallbacks - Regenerate using host=65bd71144e (same as working diagrams) - Add compressed variant for Draw.io viewers that need it - Add Mermaid markdown fallback viewable in GitHub/browser - Append AI Gateway Architecture page to enterprise-agent-ecosystem.drawio - Update README with local open instructions (GitHub links do not render drawio) Co-authored-by: Munish Kumar --- ...02-ai-gateway-mcp-design-compressed.drawio | 5 + docs/diagrams/02-ai-gateway-mcp-design.drawio | 464 +++++++-------- docs/diagrams/02-ai-gateway-mcp-design.md | 104 ++++ docs/diagrams/README.md | 8 +- .../enterprise-agent-ecosystem.drawio | 214 ++++++- docs/diagrams/generate_ai_gateway_diagram.py | 551 ++++++++---------- 6 files changed, 750 insertions(+), 596 deletions(-) create mode 100644 docs/diagrams/02-ai-gateway-mcp-design-compressed.drawio create mode 100644 docs/diagrams/02-ai-gateway-mcp-design.md diff --git a/docs/diagrams/02-ai-gateway-mcp-design-compressed.drawio b/docs/diagrams/02-ai-gateway-mcp-design-compressed.drawio new file mode 100644 index 0000000..36e4846 --- /dev/null +++ b/docs/diagrams/02-ai-gateway-mcp-design-compressed.drawio @@ -0,0 +1,5 @@ + + 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 + diff --git a/docs/diagrams/02-ai-gateway-mcp-design.drawio b/docs/diagrams/02-ai-gateway-mcp-design.drawio index 6b8214b..336a493 100644 --- a/docs/diagrams/02-ai-gateway-mcp-design.drawio +++ b/docs/diagrams/02-ai-gateway-mcp-design.drawio @@ -1,407 +1,333 @@ - - - + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - + - + - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + - + - + - + - + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + - - - - - - - - - - - - - - - - - - - - - - + - + - + - + - + - + - - - - - - - - - - + + - - + + - - + + - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + - + - + - + - + - + - - + + - - + + - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - + - + - + - - + + + + + - + + + + + + + + + + - + - - + + diff --git a/docs/diagrams/02-ai-gateway-mcp-design.md b/docs/diagrams/02-ai-gateway-mcp-design.md new file mode 100644 index 0000000..7dac0e5 --- /dev/null +++ b/docs/diagrams/02-ai-gateway-mcp-design.md @@ -0,0 +1,104 @@ +# AI Gateway + MCP Architecture + +If the `.drawio` file appears blank in GitHub or your browser, open it locally in Cursor/VS Code with the **Draw.io Integration** extension, or use [app.diagrams.net](https://app.diagrams.net) → **File → Open from → Device**. + +Also available inside the working master file: `enterprise-agent-ecosystem.drawio` → page **AI Gateway Architecture**. + +Try the compressed variant if needed: `02-ai-gateway-mcp-design-compressed.drawio` + +--- + +## Target architecture (Mermaid) + +```mermaid +flowchart TB + subgraph L1["L1 — Client Surfaces"] + UI["Web UI — am-modern-ui"] + IDE["IDE — Cursor / VS Code"] + MOB["Mobile (future)"] + PB["Portal B — ai-bots"] + end + + subgraph AUTH["Auth & Security"] + KC["Keycloak JWT + RBAC"] + GR["AI Guardrails"] + end + + subgraph L2["L2 — am-mcp-gateway :8120 (am-platform)"] + CHAT["POST /api/v1/chat/stream"] + REG["Prompt Registry — agents.yaml"] + ROUTE["Intent Router"] + SEC["AI Security"] + SSE["Unified SSE"] + LLM["LLM Proxy"] + MCP["MCP Proxy"] + CHAT --> REG --> ROUTE --> SEC --> SSE --> LLM --> MCP + end + + subgraph L3["L3 — Domain Agents (am-agents)"] + FIN["fin-agent :8100"] + TEST["ui-test-agent :8130"] + TOOL["tool-agent :8141"] + DEV["ai-bots dev :5000"] + end + + subgraph L4["L4 — MCP Core Services"] + PORT["Portfolio SDK"] + TRADE["Trade"] + VAULT["Vault MCP"] + K8S["K8s MCP"] + DB["DB MCP Toolbox"] + end + + subgraph PLAT["Platform"] + LIT["LiteLLM"] + LF["Langfuse"] + end + + UI --> KC + IDE --> KC + PB --> KC + KC --> GR --> CHAT + ROUTE --> FIN + ROUTE --> TEST + ROUTE --> TOOL + FIN --> PORT + TOOL --> VAULT + LLM --> LIT --> LF +``` + +## Chat request flow + +```mermaid +sequenceDiagram + participant U as User + participant UI as am-modern-ui + participant GW as am-mcp-gateway + participant A as fin-agent + participant M as Portfolio SDK + participant L as LiteLLM + + U->>UI: Show my portfolio PnL + UI->>GW: POST /api/v1/chat/stream + JWT + GW->>GW: Auth + RBAC + route finance + GW->>A: Proxy SSE request + A->>M: tool_call get_holdings + M-->>A: holdings data + A->>GW: POST /agent/llm/completions + GW->>L: LLM reasoning + L-->>A: completion + A-->>GW: SSE token, artifact, done + GW-->>UI: Unified SSE stream + UI-->>U: Widget + summary +``` + +## Whiteboard mapping + +| Whiteboard | Implementation | +|---|---| +| AI Gateway | `am-platform/am-mcp-gateway` | +| fin-agent | `am-fin-agent` / `fin-portfolio-agent` | +| MCP server | tool-agent + fin MCP + LiteLLM registry | +| Prompt Registry | `agents.yaml` (planned) | +| LiteLLM / billing | LiteLLM + Langfuse via gateway | +| Modern UI | `am-modern-ui` Flutter ai-chat | diff --git a/docs/diagrams/README.md b/docs/diagrams/README.md index d64cfbc..e8c275a 100644 --- a/docs/diagrams/README.md +++ b/docs/diagrams/README.md @@ -10,8 +10,12 @@ Enterprise / portal surfaces only. | [enterprise-agent-ecosystem.drawio](enterprise-agent-ecosystem.drawio) | Multi-page enterprise master | | [01-three-surfaces-overview.drawio](01-three-surfaces-overview.drawio) | Three surfaces overview | | [02-ai-gateway-mcp-design.drawio](02-ai-gateway-mcp-design.drawio) | AI Gateway + MCP design — whiteboard mapping, chat flow, LLM/MCP proxy | +| [02-ai-gateway-mcp-design-compressed.drawio](02-ai-gateway-mcp-design-compressed.drawio) | Same diagram — compressed format (use if standard file is blank) | +| [02-ai-gateway-mcp-design.md](02-ai-gateway-mcp-design.md) | Mermaid fallback — viewable in GitHub / any Markdown viewer | ## How to open -1. **Cursor / VS Code:** Draw.io Integration → open the `.drawio` file -2. **Browser:** [app.diagrams.net](https://app.diagrams.net) → Open Existing Diagram +1. **Cursor / VS Code:** Install **Draw.io Integration** extension → open the `.drawio` file locally (GitHub web links do not render diagrams) +2. **Browser:** [app.diagrams.net](https://app.diagrams.net) → File → Open from → Device +3. **If blank:** try [02-ai-gateway-mcp-design-compressed.drawio](02-ai-gateway-mcp-design-compressed.drawio) or open page **AI Gateway Architecture** in [enterprise-agent-ecosystem.drawio](enterprise-agent-ecosystem.drawio) +4. **Markdown fallback:** [02-ai-gateway-mcp-design.md](02-ai-gateway-mcp-design.md) — Mermaid diagrams viewable anywhere diff --git a/docs/diagrams/enterprise-agent-ecosystem.drawio b/docs/diagrams/enterprise-agent-ecosystem.drawio index f1a3e7a..03d5877 100644 --- a/docs/diagrams/enterprise-agent-ecosystem.drawio +++ b/docs/diagrams/enterprise-agent-ecosystem.drawio @@ -870,4 +870,216 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/diagrams/generate_ai_gateway_diagram.py b/docs/diagrams/generate_ai_gateway_diagram.py index a0adc10..d058dfc 100644 --- a/docs/diagrams/generate_ai_gateway_diagram.py +++ b/docs/diagrams/generate_ai_gateway_diagram.py @@ -1,93 +1,133 @@ #!/usr/bin/env python3 -"""Generate 02-ai-gateway-mcp-design.drawio with clear vertical/horizontal flows.""" +"""Generate draw.io using the exact format from the working repo diagram.""" + +from __future__ import annotations from pathlib import Path +from urllib.parse import quote +import base64 +import zlib OUT = Path(__file__).parent / "02-ai-gateway-mcp-design.drawio" +TEMPLATE = Path(__file__).parent / "01-three-surfaces-overview.drawio" + -STYLE = { - "title": "text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=20;fontStyle=1;fontColor=#0F172A;", - "lane": "swimlane;horizontal=1;startSize=30;fillColor={bg};strokeColor={stroke};fontColor={fg};fontStyle=1;rounded=1;", - "box_blue": "rounded=1;whiteSpace=wrap;html=1;fillColor=#0284C7;fontColor=#FFFFFF;strokeColor=#0369A1;fontSize=12;", - "box_indigo": "rounded=1;whiteSpace=wrap;html=1;fillColor=#4F46E5;fontColor=#FFFFFF;strokeColor=#312E81;fontSize=12;", - "box_purple": "rounded=1;whiteSpace=wrap;html=1;fillColor=#7C3AED;fontColor=#FFFFFF;strokeColor=#6D28D9;fontSize=12;", - "box_pink": "rounded=1;whiteSpace=wrap;html=1;fillColor=#DB2777;fontColor=#FFFFFF;strokeColor=#BE185D;fontSize=12;", - "box_teal": "rounded=1;whiteSpace=wrap;html=1;fillColor=#0D9488;fontColor=#FFFFFF;strokeColor=#0F766E;fontSize=12;", - "box_gray": "rounded=1;whiteSpace=wrap;html=1;fillColor=#64748B;fontColor=#FFFFFF;strokeColor=#475569;fontSize=12;", - "box_sky": "rounded=1;whiteSpace=wrap;html=1;fillColor=#0EA5E9;fontColor=#FFFFFF;strokeColor=#0284C7;fontSize=12;", - "box_orange": "rounded=1;whiteSpace=wrap;html=1;fillColor=#F59E0B;fontColor=#FFFFFF;strokeColor=#D97706;fontSize=12;", - "box_green": "rounded=1;whiteSpace=wrap;html=1;fillColor=#059669;fontColor=#FFFFFF;strokeColor=#047857;fontSize=12;", - "box_violet": "rounded=1;whiteSpace=wrap;html=1;fillColor=#6366F1;fontColor=#FFFFFF;strokeColor=#4F46E5;fontSize=12;", - "box_planned": "rounded=1;whiteSpace=wrap;html=1;fillColor=#EDE9FE;fontColor=#5B21B6;strokeColor=#7C3AED;dashed=1;dashPattern=8 4;fontSize=11;", - "note": "text;html=1;strokeColor=#CBD5E1;fillColor=#FFFFFF;align=left;verticalAlign=top;spacingLeft=10;spacingTop=8;fontColor=#0F172A;rounded=1;fontSize=11;", - "arrow": "edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#334155;strokeWidth=2;endArrow=classic;html=1;", - "arrow_blue": "edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#0284C7;strokeWidth=2;endArrow=classic;html=1;fontColor=#0369A1;", - "arrow_dash": "edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#6366F1;strokeWidth=2;endArrow=classic;dashed=1;html=1;fontColor=#4338CA;", - "arrow_down": "edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#334155;strokeWidth=3;endArrow=block;endFill=1;html=1;", -} - - -def esc(text: str) -> str: +def esc(value: str) -> str: return ( - text.replace("&", "&") + value.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace('"', """) - .replace("\n", " ") + .replace("\n", " ") + ) + + +def cell(cid: str, value: str, style: str, x: int, y: int, w: int, h: int, parent: str = "1") -> str: + return ( + f' \n' + f' \n' + f" " + ) + + +def edge(eid: str, source: str, target: str, label: str = "", style: str | None = None, parent: str = "1") -> str: + style = style or "strokeColor=#334155;strokeWidth=2;endArrow=classic;" + label_attr = f' value="{esc(label)}"' if label else "" + return ( + f' \n' + f' \n' + f" " ) -class Diagram: - def __init__(self, diagram_id: str, name: str, width: int = 1200, height: int = 900): - self.diagram_id = diagram_id - self.name = name - self.width = width - self.height = height - self.cells: list[str] = [] - self._n = 0 - - def uid(self, prefix: str) -> str: - self._n += 1 - return f"{prefix}{self._n}" - - def cell(self, cid: str, value: str, style: str, x: int, y: int, w: int, h: int, parent: str = "1") -> str: - self.cells.append( - f' \n' - f' \n' - f" " - ) - return cid - - def lane(self, cid: str, label: str, bg: str, stroke: str, fg: str, x: int, y: int, w: int, h: int) -> str: - style = STYLE["lane"].format(bg=bg, stroke=stroke, fg=fg) - return self.cell(cid, label, style, x, y, w, h) - - def edge(self, eid: str, source: str, target: str, label: str = "", style: str | None = None, parent: str = "1") -> None: - style = style or STYLE["arrow"] - label_attr = f' value="{esc(label)}"' if label else "" - self.cells.append( - f' \n' - f' \n' - f" " - ) - - def edge_points(self, eid: str, source: str, target: str, points: list[tuple[int, int]], label: str = "", style: str | None = None) -> None: - style = style or STYLE["arrow"] - label_attr = f' value="{esc(label)}"' if label else "" - pts = "\n".join(f' ' for x, y in points) - self.cells.append( - f' \n' - f" \n" - f" \n{pts}\n" - f" \n" - f" \n" - f" " - ) - - def render(self) -> str: - body = "\n".join(self.cells) - return f""" - +def build_page_architecture() -> str: + s_lane = "swimlane;horizontal=1;startSize=28;fillColor={bg};strokeColor={stroke};fontColor={fg};fontStyle=1;rounded=1;" + s_box = "rounded=1;whiteSpace=wrap;html=1;fillColor={bg};fontColor={fg};strokeColor={stroke};" + s_text = "text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=18;fontColor=#0F172A;" + s_note = "text;html=1;strokeColor=#CBD5E1;fillColor=#FFFFFF;align=left;verticalAlign=top;spacingLeft=8;spacingTop=6;fontColor=#0F172A;rounded=1;" + + lines: list[str] = [] + add = lines.append + + add(cell("ag_title", "AI Gateway + MCP Architecture (Target State)", s_text, 350, 20, 900, 40)) + add(cell("ag_sub", "am-platform/am-mcp-gateway is the L2 AI Gateway from the whiteboard", "text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=12;fontColor=#64748B;", 300, 55, 1000, 24)) + + add(cell("ag_lane_exec", "Request flow (6 steps)", s_lane.format(bg="#FFF7ED", stroke="#F59E0B", fg="#92400E"), 40, 90, 1520, 90)) + steps = ["1 User message", "2 Auth + RBAC", "3 Route agent", "4 Agent run", "5 MCP execute", "6 SSE response"] + xs = [20, 180, 340, 500, 660, 820] + ids = [] + for i, (lbl, x) in enumerate(zip(steps, xs)): + cid = f"ag_ex{i+1}" + ids.append(cid) + add(cell(cid, lbl, s_box.format(bg="#F59E0B", fg="#FFFFFF", stroke="#D97706"), x, 40, 130, 40, "ag_lane_exec")) + for i in range(len(ids) - 1): + add(edge(f"ag_exe{i+1}", ids[i], ids[i + 1], parent="ag_lane_exec")) + + add(cell("ag_lane_auth", "Ingress and Auth", s_lane.format(bg="#F1F5F9", stroke="#64748B", fg="#0F172A"), 40, 200, 1520, 80)) + add(cell("ag_traefik", "Traefik host routing", s_box.format(bg="#64748B", fg="#FFFFFF", stroke="#475569"), 400, 38, 140, 50, "ag_lane_auth")) + add(cell("ag_keycloak", "Keycloak JWT RBAC roles tenant", s_box.format(bg="#64748B", fg="#FFFFFF", stroke="#475569"), 580, 38, 170, 50, "ag_lane_auth")) + add(cell("ag_guard", "AI Guardrails SPT policy", s_box.format(bg="#64748B", fg="#FFFFFF", stroke="#475569"), 780, 38, 150, 50, "ag_lane_auth")) + add(edge("ag_e_tr_kc", "ag_traefik", "ag_keycloak", parent="ag_lane_auth")) + add(edge("ag_e_kc_gd", "ag_keycloak", "ag_guard", parent="ag_lane_auth")) + + add(cell("ag_lane_l1", "L1 UI - Web UI, IDE, Mobile", s_lane.format(bg="#EEF2FF", stroke="#4F46E5", fg="#312E81"), 40, 300, 1520, 110)) + add(cell("ag_ui", "Web UI am-modern-ui Flutter AI Chat Widgets", s_box.format(bg="#4F46E5", fg="#FFFFFF", stroke="#312E81"), 120, 40, 190, 65, "ag_lane_l1")) + add(cell("ag_ide", "IDE Cursor VS Code MCP client", s_box.format(bg="#4F46E5", fg="#FFFFFF", stroke="#312E81"), 350, 40, 170, 65, "ag_lane_l1")) + add(cell("ag_mobile", "Mobile future same API", s_box.format(bg="#818CF8", fg="#FFFFFF", stroke="#6366F1"), 560, 40, 150, 65, "ag_lane_l1")) + add(cell("ag_pb", "Portal B ai-bots React", s_box.format(bg="#059669", fg="#FFFFFF", stroke="#047857"), 750, 40, 170, 65, "ag_lane_l1")) + add(cell("ag_pc", "Portal C kagent SRE", s_box.format(bg="#EA580C", fg="#FFFFFF", stroke="#C2410C"), 960, 40, 150, 65, "ag_lane_l1")) + + add(cell("ag_lane_l2", "L2 AI Gateway - am-mcp-gateway :8120 (am-platform repo)", s_lane.format(bg="#F0F9FF", stroke="#0284C7", fg="#0C4A6E"), 40, 430, 1520, 130)) + add(cell("ag_gw", "am-mcp-gateway POST /api/v1/chat/stream POST /api/v1/agent/llm/completions", s_box.format(bg="#0284C7", fg="#FFFFFF", stroke="#0369A1"), 460, 38, 240, 55, "ag_lane_l2")) + add(cell("ag_preg", "Prompt Registry agents.yaml", s_box.format(bg="#0EA5E9", fg="#FFFFFF", stroke="#0284C7"), 120, 95, 130, 40, "ag_lane_l2")) + add(cell("ag_router", "Intent router", s_box.format(bg="#0EA5E9", fg="#FFFFFF", stroke="#0284C7"), 280, 95, 120, 40, "ag_lane_l2")) + add(cell("ag_rbac", "RBAC check", s_box.format(bg="#0EA5E9", fg="#FFFFFF", stroke="#0284C7"), 430, 95, 120, 40, "ag_lane_l2")) + add(cell("ag_sse", "Unified SSE", s_box.format(bg="#0EA5E9", fg="#FFFFFF", stroke="#0284C7"), 580, 95, 120, 40, "ag_lane_l2")) + add(cell("ag_llm", "LLM proxy", s_box.format(bg="#0EA5E9", fg="#FFFFFF", stroke="#0284C7"), 730, 95, 120, 40, "ag_lane_l2")) + add(cell("ag_mcp", "MCP proxy", s_box.format(bg="#0EA5E9", fg="#FFFFFF", stroke="#0284C7"), 880, 95, 120, 40, "ag_lane_l2")) + add(edge("ag_e_gw_r", "ag_gw", "ag_router", parent="ag_lane_l2")) + add(edge("ag_e_r_rb", "ag_router", "ag_rbac", parent="ag_lane_l2")) + add(edge("ag_e_rb_s", "ag_rbac", "ag_sse", parent="ag_lane_l2")) + add(edge("ag_e_s_l", "ag_sse", "ag_llm", parent="ag_lane_l2")) + add(edge("ag_e_l_m", "ag_llm", "ag_mcp", parent="ag_lane_l2")) + + add(cell("ag_lane_l3", "L3 Domain Agents - am-agents repo", s_lane.format(bg="#F5F3FF", stroke="#7C3AED", fg="#4C1D95"), 40, 580, 1520, 90)) + add(cell("ag_fin", "fin-agent :8100", s_box.format(bg="#DB2777", fg="#FFFFFF", stroke="#BE185D"), 80, 40, 130, 40, "ag_lane_l3")) + add(cell("ag_test", "ui-test-agent :8130", s_box.format(bg="#7C3AED", fg="#FFFFFF", stroke="#6D28D9"), 240, 40, 140, 40, "ag_lane_l3")) + add(cell("ag_tool", "tool-agent :8141", s_box.format(bg="#7C3AED", fg="#FFFFFF", stroke="#6D28D9"), 410, 40, 130, 40, "ag_lane_l3")) + add(cell("ag_dev", "ai-bots dev :5000", s_box.format(bg="#059669", fg="#FFFFFF", stroke="#047857"), 570, 40, 140, 40, "ag_lane_l3")) + add(cell("ag_scribe", "scribe planned", s_box.format(bg="#EDE9FE", fg="#5B21B6", stroke="#7C3AED"), 740, 40, 120, 40, "ag_lane_l3")) + add(cell("ag_support", "support planned", s_box.format(bg="#EDE9FE", fg="#5B21B6", stroke="#7C3AED"), 890, 40, 130, 40, "ag_lane_l3")) + + add(cell("ag_lane_l4", "L4 MCP Core Services", s_lane.format(bg="#F0FDFA", stroke="#0D9488", fg="#134E4A"), 40, 690, 1520, 90)) + add(cell("ag_port", "Portfolio SDK", s_box.format(bg="#0D9488", fg="#FFFFFF", stroke="#0F766E"), 80, 40, 130, 40, "ag_lane_l4")) + add(cell("ag_trade", "Trade", s_box.format(bg="#0D9488", fg="#FFFFFF", stroke="#0F766E"), 240, 40, 100, 40, "ag_lane_l4")) + add(cell("ag_market", "Market Analysis", s_box.format(bg="#0D9488", fg="#FFFFFF", stroke="#0F766E"), 370, 40, 140, 40, "ag_lane_l4")) + add(cell("ag_vault", "Vault MCP", s_box.format(bg="#0D9488", fg="#FFFFFF", stroke="#0F766E"), 540, 40, 110, 40, "ag_lane_l4")) + add(cell("ag_k8s", "K8s MCP", s_box.format(bg="#0D9488", fg="#FFFFFF", stroke="#0F766E"), 680, 40, 100, 40, "ag_lane_l4")) + add(cell("ag_db", "DB MCP Toolbox", s_box.format(bg="#0D9488", fg="#FFFFFF", stroke="#0F766E"), 810, 40, 140, 40, "ag_lane_l4")) + + add(cell("ag_lane_obs", "Platform - LiteLLM Langfuse Vault", s_lane.format(bg="#EEF2FF", stroke="#6366F1", fg="#312E81"), 40, 800, 520, 80)) + add(cell("ag_lit", "LiteLLM", s_box.format(bg="#6366F1", fg="#FFFFFF", stroke="#4F46E5"), 30, 38, 120, 40, "ag_lane_obs")) + add(cell("ag_lf", "Langfuse", s_box.format(bg="#6366F1", fg="#FFFFFF", stroke="#4F46E5"), 180, 38, 120, 40, "ag_lane_obs")) + add(cell("ag_vlt", "Vault secrets", s_box.format(bg="#6366F1", fg="#FFFFFF", stroke="#4F46E5"), 330, 38, 120, 40, "ag_lane_obs")) + + add(edge("ag_e_ui_gw", "ag_ui", "ag_gw", "chat/stream", "strokeColor=#4F46E5;strokeWidth=2;endArrow=classic;")) + add(edge("ag_e_pb_gw", "ag_pb", "ag_gw", "all agents", "strokeColor=#059669;strokeWidth=2;endArrow=classic;")) + add(edge("ag_e_kc_ui", "ag_keycloak", "ag_ui", parent="1")) + add(edge("ag_e_gd_gw", "ag_guard", "ag_gw", parent="1")) + add(edge("ag_e_sse_fin", "ag_sse", "ag_fin", parent="1")) + add(edge("ag_e_sse_test", "ag_sse", "ag_test", parent="1")) + add(edge("ag_e_fin_port", "ag_fin", "ag_port", parent="1")) + add(edge("ag_e_tool_vault", "ag_tool", "ag_vault", parent="1")) + add(edge("ag_e_llm_lit", "ag_llm", "ag_lit", "LLM calls", "strokeColor=#6366F1;strokeWidth=1;endArrow=classic;dashed=1;")) + add(edge("ag_e_lit_lf", "ag_lit", "ag_lf", parent="1")) + + add(cell("ag_legend", "Legend: Blue=Gateway Purple=Agents Teal=MCP Indigo=UI Green=Portal B Orange=Portal C", s_note, 580, 800, 520, 60)) + + body = "\n".join(lines) + return f""" + @@ -97,260 +137,123 @@ def render(self) -> str: """ -def build_target_architecture() -> Diagram: - d = Diagram("target-architecture", "1 - Target Architecture", 1200, 1300) +def build_page_chat_flow() -> str: + s_box = "rounded=1;whiteSpace=wrap;html=1;fillColor={bg};fontColor={fg};strokeColor={stroke};" + lines = [] + add = lines.append + + add(cell("cf_t", "Chat Flow - Show my portfolio PnL", "text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=16;fontColor=#0F172A;", 300, 20, 500, 30)) + add(cell("cf_user", "User", s_box.format(bg="#64748B", fg="#FFFFFF", stroke="#475569"), 400, 60, 120, 40)) + add(cell("cf_ui", "L1 am-modern-ui", s_box.format(bg="#4F46E5", fg="#FFFFFF", stroke="#312E81"), 380, 130, 160, 45)) + add(cell("cf_gw", "L2 am-mcp-gateway POST /api/v1/chat/stream", s_box.format(bg="#0284C7", fg="#FFFFFF", stroke="#0369A1"), 360, 210, 200, 50)) + add(cell("cf_router", "Intent router picks finance", s_box.format(bg="#0EA5E9", fg="#FFFFFF", stroke="#0284C7"), 360, 290, 200, 45)) + add(cell("cf_agent", "L3 fin-agent :8100", s_box.format(bg="#DB2777", fg="#FFFFFF", stroke="#BE185D"), 380, 370, 160, 45)) + add(cell("cf_mcp", "L4 Portfolio SDK MCP", s_box.format(bg="#0D9488", fg="#FFFFFF", stroke="#0F766E"), 140, 370, 170, 45)) + add(cell("cf_lit", "LiteLLM via gateway", s_box.format(bg="#6366F1", fg="#FFFFFF", stroke="#4F46E5"), 620, 370, 160, 45)) + add(cell("cf_sse", "SSE stream token artifact done", s_box.format(bg="#F59E0B", fg="#FFFFFF", stroke="#D97706"), 380, 460, 160, 50)) + add(cell("cf_note", "Agents call MCP_GATEWAY_BASE_URL for LLM at /api/v1/agent/llm/completions", "text;html=1;strokeColor=#CBD5E1;fillColor=#FFFFFF;align=left;spacingLeft=8;fontColor=#475569;rounded=1;", 120, 540, 720, 40)) + + for i, (a, b) in enumerate([ + ("cf_user", "cf_ui"), ("cf_ui", "cf_gw"), ("cf_gw", "cf_router"), ("cf_router", "cf_agent"), + ("cf_agent", "cf_mcp"), ("cf_agent", "cf_lit"), ("cf_agent", "cf_sse"), ("cf_sse", "cf_ui"), + ]): + add(edge(f"cf_e{i+1}", a, b)) + + body = "\n".join(lines) + return f""" + + + + +{body} + + + """ - d.cell("p1_title", "AI Gateway + MCP — Target Architecture", STYLE["title"], 150, 20, 900, 40) - d.cell( - "p1_sub", - "Whiteboard vision implemented via am-platform/am-mcp-gateway (L2) routing am-agents (L3) to MCP core services (L4)", - "text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=12;fontColor=#64748B;", - 80, - 55, - 1040, - 30, - ) - # L1 - l1 = d.lane("p1_l1", "L1 — Client Surfaces (UI · IDE · Mobile)", "#EEF2FF", "#4F46E5", "#312E81", 40, 100, 1120, 110) - ui = d.cell("p1_ui", "Web UI\nam-modern-ui\nAI Chat · Widgets", STYLE["box_indigo"], 60, 45, 180, 55, l1) - ide = d.cell("p1_ide", "IDE\nCursor / VS Code\nMCP tools", STYLE["box_indigo"], 270, 45, 160, 55, l1) - mob = d.cell("p1_mob", "Mobile\n(future)\nSame API", STYLE["box_planned"], 460, 45, 130, 55, l1) - pb = d.cell("p1_pb", "Portal B\nai-bots React\nWorkspace", STYLE["box_green"], 620, 45, 160, 55, l1) - pc = d.cell("p1_pc", "Portal C\nkagent UI\nSRE ops", "rounded=1;whiteSpace=wrap;html=1;fillColor=#EA580C;fontColor=#FFFFFF;strokeColor=#C2410C;fontSize=12;", 810, 45, 140, 55, l1) - - # Auth strip - lauth = d.lane("p1_auth", "Auth & Security — Keycloak JWT · RBAC · Guardrails", "#F1F5F9", "#64748B", "#0F172A", 40, 230, 1120, 80) - kc = d.cell("p1_kc", "Keycloak\nJWT + roles", STYLE["box_gray"], 420, 40, 140, 35, lauth) - rbac = d.cell("p1_rbac", "RBAC\ntenant · agent allowlist", STYLE["box_gray"], 590, 40, 170, 35, lauth) - - # L2 Gateway - internal flow left to right - l2 = d.lane("p1_l2", "L2 — AI Gateway am-mcp-gateway :8120 (am-platform repo)", "#F0F9FF", "#0284C7", "#0C4A6E", 40, 330, 1120, 160) - chat = d.cell("p1_chat", "POST\n/api/v1/chat/stream", STYLE["box_blue"], 40, 45, 130, 50, l2) - preg = d.cell("p1_preg", "Prompt Registry\nagents.yaml", STYLE["box_sky"], 190, 45, 130, 50, l2) - route = d.cell("p1_route", "Intent Router\nkeywords + LLM", STYLE["box_sky"], 340, 45, 130, 50, l2) - sec = d.cell("p1_sec", "AI Security\ninput/output guard", STYLE["box_sky"], 490, 45, 130, 50, l2) - sse = d.cell("p1_sse", "Unified SSE\nnormalize events", STYLE["box_sky"], 640, 45, 130, 50, l2) - llm = d.cell("p1_llm", "LLM Proxy\n/agent/llm/completions", STYLE["box_sky"], 790, 45, 150, 50, l2) - mcp = d.cell("p1_mcp", "MCP Proxy\ntool routes", STYLE["box_sky"], 960, 45, 130, 50, l2) - d.edge("p1_e_l2_1", chat, preg, parent=l2) - d.edge("p1_e_l2_2", preg, route, parent=l2) - d.edge("p1_e_l2_3", route, sec, parent=l2) - d.edge("p1_e_l2_4", sec, sse, parent=l2) - d.edge("p1_e_l2_5", sse, llm, parent=l2) - d.edge("p1_e_l2_6", llm, mcp, parent=l2) - - # L3 agents - l3 = d.lane("p1_l3", "L3 — Domain Agents (am-agents repo) — gateway routes by intent", "#F5F3FF", "#7C3AED", "#4C1D95", 40, 510, 1120, 110) - fin = d.cell("p1_fin", "fin-agent :8100\nPortfolio · widgets", STYLE["box_pink"], 60, 45, 150, 55, l3) - test = d.cell("p1_test", "ui-test-agent :8130\nPlaywright QA", STYLE["box_purple"], 240, 45, 150, 55, l3) - tool = d.cell("p1_tool", "tool-agent :8141\nVault · K8s · Mongo", STYLE["box_purple"], 420, 45, 150, 55, l3) - dev = d.cell("p1_dev", "ai-bots dev :5000\nCode · Jira · RAG", STYLE["box_green"], 600, 45, 150, 55, l3) - scribe = d.cell("p1_scribe", "scribe-agent\n(planned)", STYLE["box_planned"], 780, 45, 130, 55, l3) - sup = d.cell("p1_sup", "support-agent\n(planned)", STYLE["box_planned"], 930, 45, 130, 55, l3) - - # L4 MCP - l4 = d.lane("p1_l4", "L4 — MCP Core Services (Whiteboard: Portfolio · Trade · Market · Stock Analysis)", "#F0FDFA", "#0D9488", "#134E4A", 40, 640, 1120, 110) - port = d.cell("p1_port", "Portfolio SDK\nholdings · PnL", STYLE["box_teal"], 60, 45, 130, 55, l4) - trade = d.cell("p1_trade", "Trade\norders", STYLE["box_teal"], 210, 45, 100, 55, l4) - market = d.cell("p1_market", "Market Analysis\nETF · MF", STYLE["box_teal"], 330, 45, 130, 55, l4) - stock = d.cell("p1_stock", "Stock Analysis", STYLE["box_teal"], 480, 45, 120, 55, l4) - vault = d.cell("p1_vault", "Vault MCP\nsecrets", STYLE["box_teal"], 620, 45, 110, 55, l4) - k8s = d.cell("p1_k8s", "K8s MCP\ndeploy", STYLE["box_teal"], 750, 45, 100, 55, l4) - dbmcp = d.cell("p1_db", "DB MCP Toolbox\nPostgres · Mongo", STYLE["box_teal"], 870, 45, 150, 55, l4) - - # Platform - lplat = d.lane("p1_plat", "Platform — LiteLLM · Langfuse · Vault · Usage/Billing", "#EEF2FF", "#6366F1", "#312E81", 40, 770, 1120, 90) - lit = d.cell("p1_lit", "LiteLLM\nmodel routing", STYLE["box_violet"], 200, 40, 140, 45, lplat) - lf = d.cell("p1_lf", "Langfuse\ntraces · cost", STYLE["box_violet"], 380, 40, 140, 45, lplat) - hv = d.cell("p1_hv", "Vault\nsecrets", STYLE["box_violet"], 560, 40, 120, 45, lplat) - - # Vertical flow arrows between layers - d.edge("p1_v1", ui, kc, "all clients", STYLE["arrow_down"]) - d.edge("p1_v2", kc, chat, "authenticated", STYLE["arrow_down"]) - d.edge("p1_v3", route, fin, "route: finance", STYLE["arrow_down"]) - d.edge("p1_v4", route, test, "route: test", STYLE["arrow_down"]) - d.edge("p1_v5", route, tool, "route: devops", STYLE["arrow_down"]) - d.edge("p1_v6", fin, port, "MCP tools", STYLE["arrow_down"]) - d.edge("p1_v7", tool, vault, "MCP tools", STYLE["arrow_down"]) - d.edge("p1_v8", llm, lit, "LLM calls", STYLE["arrow_dash"]) - d.edge("p1_v9", lit, lf, "trace", STYLE["arrow_dash"]) - - d.cell( - "p1_legend", - "FLOW: Clients -> Auth -> am-mcp-gateway (route) -> Domain Agent -> MCP Service -> LiteLLM/Langfuse\n" - "TODAY: fin-agent, ui-test-agent, tool-agent live. PLANNED: agents.yaml registry, scribe, unified SSE schema.", - STYLE["note"], - 40, - 880, - 1120, - 55, - ) - d.cell( - "p1_map", - "Whiteboard mapping:\n" - "AI Gateway = am-mcp-gateway | fin-agent = am-fin-agent | MCP server = tool-agent + fin MCP + LiteLLM registry | " - "Prompt Registry = agents.yaml | Modern UI = am-modern-ui", - "text;html=1;strokeColor=#F59E0B;fillColor=#FFFBEB;align=left;verticalAlign=top;spacingLeft=10;spacingTop=8;fontColor=#92400E;rounded=1;fontSize=11;", - 40, - 950, - 1120, - 70, - ) - return d - - -def build_chat_flow() -> Diagram: - d = Diagram("chat-flow", "2 - Chat Request Flow", 1200, 850) - - d.cell("p2_title", "Chat Request Flow — Example: Show my portfolio PnL", STYLE["title"], 200, 20, 800, 40) - - # Step pipeline - lane = d.lane("p2_lane", "6-Step Execution Pipeline", "#FFF7ED", "#F59E0B", "#92400E", 40, 70, 1120, 80) - steps = [] - labels = ["1 User msg", "2 Auth", "3 Route", "4 Agent", "5 MCP exec", "6 SSE back"] - x = 30 - for i, lbl in enumerate(labels): - cid = f"p2_s{i+1}" - steps.append(d.cell(cid, lbl, STYLE["box_orange"], x, 40, 110, 35, lane)) - x += 130 - for i in range(len(steps) - 1): - d.edge(f"p2_es{i+1}", steps[i], steps[i + 1], parent=lane) - - # Main horizontal sequence - user = d.cell("p2_user", "User", "shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fillColor=#4F46E5;strokeColor=#312E81;", 60, 200, 40, 70) - ui = d.cell("p2_ui", "L1 UI\nam-modern-ui", STYLE["box_indigo"], 140, 210, 120, 55) - gw = d.cell("p2_gw", "L2 Gateway\nam-mcp-gateway\n:8120", STYLE["box_blue"], 300, 200, 150, 70) - agent = d.cell("p2_agent", "L3 fin-agent\n:8100", STYLE["box_pink"], 490, 210, 120, 55) - mcp = d.cell("p2_mcp", "L4 Portfolio SDK", STYLE["box_teal"], 650, 210, 130, 55) - lit = d.cell("p2_lit", "LiteLLM", STYLE["box_violet"], 820, 210, 100, 55) - lf = d.cell("p2_lf", "Langfuse", STYLE["box_violet"], 960, 210, 100, 55) - - d.edge("p2_e1", user, ui, "1") - d.edge("p2_e2", ui, gw, "2 POST /chat/stream\n+ JWT", STYLE["arrow_blue"]) - d.edge("p2_e3", gw, agent, "3 route: finance\nagent_selected", STYLE["arrow_blue"]) - d.edge("p2_e4", agent, mcp, "4 tool_call:\nget_holdings") - d.edge("p2_e5", agent, lit, "5 LLM via\n/agent/llm/completions", STYLE["arrow_dash"]) - d.edge("p2_e6", lit, lf, "trace") - d.edge_points("p2_e7", agent, ui, [(540, 340), (200, 340)], "6 SSE: token, artifact(widget), done", STYLE["arrow_blue"]) - - d.cell( - "p2_sse", - "SSE Events (gateway normalizes all agents to one schema):\n" - "agent_selected | stage | tool_call | token | artifact | memory_update | done", - STYLE["note"], - 40, - 390, - 540, - 70, - ) - d.cell( - "p2_req", - "Gateway -> Agent request body:\n" - "message, sessionId, userId, traceId, agentContext { pinnedAgent, requirementIds }", - STYLE["note"], - 600, - 390, - 540, - 70, - ) +def build_page_llm_mcp() -> str: + s_box = "rounded=1;whiteSpace=wrap;html=1;fillColor={bg};fontColor={fg};strokeColor={stroke};" + s_lane = "swimlane;horizontal=1;startSize=28;fillColor={bg};strokeColor={stroke};fontColor={fg};fontStyle=1;rounded=1;" + lines = [] + add = lines.append + + add(cell("lm_t", "LLM and MCP Proxy Paths", "text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=16;fontColor=#0F172A;", 250, 20, 500, 30)) + + add(cell("lm_lane_a", "Path A - Agent LLM calls", s_lane.format(bg="#EEF2FF", stroke="#6366F1", fg="#312E81"), 40, 70, 820, 100)) + add(cell("lm_agents", "ui-test tool db fin agents", s_box.format(bg="#7C3AED", fg="#FFFFFF", stroke="#6D28D9"), 30, 45, 170, 45, "lm_lane_a")) + add(cell("lm_gw", "am-mcp-gateway /agent/llm/completions", s_box.format(bg="#0284C7", fg="#FFFFFF", stroke="#0369A1"), 230, 45, 190, 45, "lm_lane_a")) + add(cell("lm_lit", "LiteLLM", s_box.format(bg="#6366F1", fg="#FFFFFF", stroke="#4F46E5"), 450, 45, 120, 45, "lm_lane_a")) + add(cell("lm_lf", "Langfuse", s_box.format(bg="#6366F1", fg="#FFFFFF", stroke="#4F46E5"), 600, 45, 120, 45, "lm_lane_a")) + add(edge("lm_a1", "lm_agents", "lm_gw", parent="lm_lane_a")) + add(edge("lm_a2", "lm_gw", "lm_lit", parent="lm_lane_a")) + add(edge("lm_a3", "lm_lit", "lm_lf", parent="lm_lane_a")) + + add(cell("lm_lane_b", "Path B - MCP tool registration", s_lane.format(bg="#F0FDFA", stroke="#0D9488", fg="#134E4A"), 40, 190, 820, 100)) + add(cell("lm_sync", "sync_litellm_mcp_tools", s_box.format(bg="#0D9488", fg="#FFFFFF", stroke="#0F766E"), 30, 45, 170, 45, "lm_lane_b")) + add(cell("lm_reg", "LiteLLM MCP registry", s_box.format(bg="#6366F1", fg="#FFFFFF", stroke="#4F46E5"), 230, 45, 160, 45, "lm_lane_b")) + add(cell("lm_gw2", "gateway MCP proxy", s_box.format(bg="#0284C7", fg="#FFFFFF", stroke="#0369A1"), 420, 45, 150, 45, "lm_lane_b")) + add(cell("lm_ide", "Cursor IDE MCP client", s_box.format(bg="#4F46E5", fg="#FFFFFF", stroke="#312E81"), 600, 45, 150, 45, "lm_lane_b")) + add(edge("lm_b1", "lm_sync", "lm_reg", parent="lm_lane_b")) + add(edge("lm_b2", "lm_reg", "lm_gw2", parent="lm_lane_b")) + add(edge("lm_b3", "lm_ide", "lm_gw2", parent="lm_lane_b")) + + body = "\n".join(lines) + return f""" + + + + +{body} + + + """ - d.cell( - "p2_modes", - "No-agent mode: gateway answers via LiteLLM directly (simple queries).\n" - "Multi-agent mode: router chains agents using agentContext.workingSummary.", - "text;html=1;strokeColor=#CBD5E1;fillColor=#F8FAFC;align=left;verticalAlign=top;spacingLeft=10;spacingTop=8;fontColor=#475569;rounded=1;fontSize=11;", - 40, - 480, - 1100, - 50, - ) - # agents.yaml - d.cell( - "p2_yaml", - "agents.yaml registry (planned — replaces hardcoded routes):\n" - "finance -> am-fin-agent:8100 keywords: [portfolio, pnl, trade]\n" - "test -> am-ui-test-agent:8130 keywords: [playwright, ui, test]\n" - "devops -> am-tool-agent:8141 keywords: [k8s, vault, deploy]", - "text;html=1;strokeColor=#0284C7;fillColor=#F0F9FF;align=left;verticalAlign=top;spacingLeft=10;spacingTop=8;fontColor=#0C4A6E;rounded=1;fontSize=11;", - 40, - 550, - 1100, - 80, - ) - return d - - -def build_llm_mcp_flow() -> Diagram: - d = Diagram("llm-mcp-flow", "3 - LLM and MCP Paths", 1200, 900) - - d.cell("p3_title", "LLM & MCP Proxy Paths — How agents use the gateway today", STYLE["title"], 150, 20, 900, 40) - - # Path A - la = d.lane("p3_la", "Path A — Agent LLM calls (MCP_GATEWAY_BASE_URL)", "#EEF2FF", "#6366F1", "#312E81", 40, 70, 1120, 130) - agents = d.cell("p3_agents", "ui-test-agent\ntool-agent\ndb-agent\nfin-portfolio-agent", STYLE["box_purple"], 40, 45, 160, 70, la) - gw = d.cell("p3_gw", "am-mcp-gateway\nPOST /api/v1/agent/llm/completions", STYLE["box_blue"], 250, 45, 220, 70, la) - lit = d.cell("p3_lit", "LiteLLM\nOpenAI · Gemini · Together", STYLE["box_violet"], 520, 45, 180, 70, la) - lf = d.cell("p3_lf", "Langfuse\ntraces · sessionId · cost", STYLE["box_violet"], 750, 45, 160, 70, la) - env = d.cell("p3_env", "Env: MCP_GATEWAY_BASE_URL\nAuth: Keycloak client_credentials\nVault: LITELLM_MASTER_KEY", STYLE["note"], 940, 45, 150, 70, la) - d.edge("p3_a1", agents, gw, "HTTP POST", parent=la) - d.edge("p3_a2", gw, lit, "proxy", parent=la) - d.edge("p3_a3", lit, lf, "observability", parent=la) - - # Path B - lb = d.lane("p3_lb", "Path B — MCP tool registration & execution", "#F0FDFA", "#0D9488", "#134E4A", 40, 220, 1120, 150) - sync = d.cell("p3_sync", "sync_litellm_mcp_tools\n(am-mcp-gateway)", STYLE["box_teal"], 40, 45, 170, 55, lb) - reg = d.cell("p3_reg", "LiteLLM MCP Registry\nK8s ConfigMap", STYLE["box_violet"], 240, 45, 170, 55, lb) - gw2 = d.cell("p3_gw2", "am-mcp-gateway\nMCP proxy routes", STYLE["box_blue"], 440, 45, 170, 55, lb) - cursor = d.cell("p3_cursor", "Cursor / IDE\nMCP client", STYLE["box_indigo"], 640, 45, 130, 55, lb) - finm = d.cell("p3_finm", "fin MCP\nportfolio tools", STYLE["box_teal"], 40, 105, 120, 40, lb) - uim = d.cell("p3_uim", "ui-test MCP", STYLE["box_teal"], 180, 105, 110, 40, lb) - toolm = d.cell("p3_toolm", "tool-agent MCP\nvault · k8s", STYLE["box_teal"], 310, 105, 130, 40, lb) - dbm = d.cell("p3_dbm", "DB MCP Toolbox", STYLE["box_teal"], 460, 105, 130, 40, lb) - d.edge("p3_b1", sync, reg, "register", parent=lb) - d.edge("p3_b2", reg, gw2, "expose", parent=lb) - d.edge("p3_b3", cursor, gw2, "MCP protocol", parent=lb) - d.edge("p3_b4", finm, reg, parent=lb) - d.edge("p3_b5", uim, reg, parent=lb) - d.edge("p3_b6", toolm, reg, parent=lb) - - # Path C security - lc = d.lane("p3_lc", "Path C — Security loop (every request wraps LiteLLM)", "#F1F5F9", "#64748B", "#0F172A", 40, 390, 1120, 100) - boxes = [] - labels = ["Request", "JWT + RBAC", "SPT Guard", "Prompt filter", "LiteLLM", "Output guard", "Response"] - x = 30 - for lbl in labels: - style = STYLE["box_violet"] if lbl == "LiteLLM" else STYLE["box_gray"] - if lbl == "Response": - style = STYLE["box_blue"] - boxes.append(d.cell(f"p3_c_{lbl.replace(' ', '_')}", lbl, style, x, 45, 110, 45, lc)) - x += 130 - for i in range(len(boxes) - 1): - d.edge(f"p3_c_e{i+1}", boxes[i], boxes[i + 1], parent=lc) - - d.cell( - "p3_note", - "K8s service: am-mcp-gateway.am-apps-{env}.svc.cluster.local:8120\n" - "All agents in am-agents set MCP_GATEWAY_BASE_URL in Helm values (dev/preprod/prod)", - STYLE["note"], - 40, - 510, - 1100, - 50, - ) - return d +def compress_drawio(xml: str) -> str: + encoded = quote(xml, safe="") + co = zlib.compressobj(level=9, wbits=-15) + compressed = co.compress(encoded.encode("utf-8")) + co.flush() + return base64.b64encode(compressed).decode("ascii") + + +def wrap_mxfile(pages: list[str], compressed: bool = False) -> str: + if not compressed: + return "\n" + "\n".join(pages) + "\n\n" + + wrapped_pages = [] + for page in pages: + # extract inner mxGraphModel xml + start = page.index("") + len("") + inner = page[start:end] + diagram_open = page[: page.index(">") + 1] + diagram_close = " " + encoded = compress_drawio(inner) + wrapped_pages.append(f"{diagram_open}{encoded}{diagram_close}") + return "\n" + "\n".join(wrapped_pages) + "\n\n" + + +def append_to_enterprise(page_xml: str) -> None: + ent = Path(__file__).parent / "enterprise-agent-ecosystem.drawio" + text = ent.read_text(encoding="utf-8") + if "ai-gateway-architecture" in text: + return + if text.rstrip().endswith(""): + text = text.rstrip()[:-len("")] + page_xml + "\n\n" + ent.write_text(text, encoding="utf-8") def main() -> None: - pages = [build_target_architecture(), build_chat_flow(), build_llm_mcp_flow()] - content = "\n".join(p.render() for p in pages) - xml = f""" -{content} - -""" - OUT.write_text(xml, encoding="utf-8") - print(f"Wrote {OUT} ({len(xml)} bytes, {len(pages)} pages)") + pages = [build_page_architecture(), build_page_chat_flow(), build_page_llm_mcp()] + OUT.write_text(wrap_mxfile(pages, compressed=False), encoding="utf-8") + + compressed_out = OUT.with_name("02-ai-gateway-mcp-design-compressed.drawio") + compressed_out.write_text(wrap_mxfile(pages, compressed=True), encoding="utf-8") + + append_to_enterprise(build_page_architecture()) + print(f"Wrote {OUT}") + print(f"Wrote {compressed_out}") + print("Appended architecture page to enterprise-agent-ecosystem.drawio") if __name__ == "__main__":