From b9a924cf4af23fe45de08c7eface1a311d860ce5 Mon Sep 17 00:00:00 2001 From: Jonathan Boniface Date: Wed, 2 Sep 2026 16:08:12 +0100 Subject: [PATCH] fix: refactored manually --- app/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 210 bytes app/__pycache__/agent.cpython-312.pyc | Bin 0 -> 2351 bytes app/__pycache__/card.cpython-312.pyc | Bin 0 -> 1156 bytes app/__pycache__/config.cpython-312.pyc | Bin 0 -> 2248 bytes app/agent.py | 50 +++++++- app/card.py | 37 +++++- app/config.py | 50 +++++++- app/nodes/__init__.py | 8 ++ .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 439 bytes .../__pycache__/design_node.cpython-312.pyc | Bin 0 -> 4359 bytes .../__pycache__/discover_node.cpython-312.pyc | Bin 0 -> 3691 bytes .../__pycache__/package_node.cpython-312.pyc | Bin 0 -> 3100 bytes .../__pycache__/validate_node.cpython-312.pyc | Bin 0 -> 3067 bytes app/nodes/design_node.py | 114 ++++++++++++++++++ app/nodes/discover_node.py | 74 ++++++++++++ app/nodes/package_node.py | 73 +++++++++++ app/nodes/validate_node.py | 60 +++++++++ app/skills/__init__.py | 5 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 353 bytes app/skills/__pycache__/loader.cpython-312.pyc | Bin 0 -> 5644 bytes app/skills/architecture_design/SKILL.md | 22 ++++ app/skills/loader.py | 105 ++++++++++++++++ app/skills/packaging_guide/SKILL.md | 20 +++ app/skills/requirements_discovery/SKILL.md | 19 +++ app/skills/validation_rules/SKILL.md | 19 +++ app/states/__init__.py | 5 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 331 bytes app/states/__pycache__/state.cpython-312.pyc | Bin 0 -> 1292 bytes app/states/state.py | 27 +++++ app/tools/__init__.py | 13 ++ .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 423 bytes .../validation_tools.cpython-312.pyc | Bin 0 -> 4556 bytes app/tools/validation_tools.py | 109 +++++++++++++++++ app/workflows/__init__.py | 5 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 396 bytes .../gcp_architecture_graph.cpython-312.pyc | Bin 0 -> 3408 bytes .../__pycache__/routes.cpython-312.pyc | Bin 0 -> 4190 bytes app/workflows/gcp_architecture_graph.py | 56 +++++++++ app/workflows/routes.py | 82 +++++++++++++ eval/__init__.py | 1 + eval/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 239 bytes eval/__pycache__/eval_harness.cpython-312.pyc | Bin 0 -> 4117 bytes eval/__pycache__/metrics.cpython-312.pyc | Bin 0 -> 4899 bytes eval/__pycache__/optimizer.cpython-312.pyc | Bin 0 -> 2950 bytes ..._eval_harness.cpython-312-pytest-9.1.1.pyc | Bin 0 -> 3978 bytes eval/datasets/benchmark_cases.json | 26 ++++ eval/eval_harness.py | 81 +++++++++++++ eval/metrics.py | 100 +++++++++++++++ eval/optimizer.py | 58 +++++++++ eval/test_eval_harness.py | 32 +++++ requirements-dev.txt | 5 + requirements.txt | 11 ++ solution-architecture-guide.md | 8 +- tests/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 212 bytes .../conftest.cpython-312-pytest-9.1.1.pyc | Bin 0 -> 212 bytes ...est_agent_api.cpython-312-pytest-9.1.1.pyc | Bin 0 -> 8237 bytes ...est_artifacts.cpython-312-pytest-9.1.1.pyc | Bin 0 -> 3054 bytes ...raph_workflow.cpython-312-pytest-9.1.1.pyc | Bin 0 -> 8464 bytes .../test_package.cpython-312-pytest-9.1.1.pyc | Bin 0 -> 2940 bytes ..._skill_loader.cpython-312-pytest-9.1.1.pyc | Bin 0 -> 8185 bytes tests/test_agent_api.py | 52 ++++++++ tests/test_graph_workflow.py | 32 +++++ tests/test_skill_loader.py | 45 +++++++ 63 files changed, 1398 insertions(+), 6 deletions(-) create mode 100644 app/__pycache__/__init__.cpython-312.pyc create mode 100644 app/__pycache__/agent.cpython-312.pyc create mode 100644 app/__pycache__/card.cpython-312.pyc create mode 100644 app/__pycache__/config.cpython-312.pyc create mode 100644 app/nodes/__pycache__/__init__.cpython-312.pyc create mode 100644 app/nodes/__pycache__/design_node.cpython-312.pyc create mode 100644 app/nodes/__pycache__/discover_node.cpython-312.pyc create mode 100644 app/nodes/__pycache__/package_node.cpython-312.pyc create mode 100644 app/nodes/__pycache__/validate_node.cpython-312.pyc create mode 100644 app/nodes/design_node.py create mode 100644 app/nodes/discover_node.py create mode 100644 app/nodes/package_node.py create mode 100644 app/nodes/validate_node.py create mode 100644 app/skills/__init__.py create mode 100644 app/skills/__pycache__/__init__.cpython-312.pyc create mode 100644 app/skills/__pycache__/loader.cpython-312.pyc create mode 100644 app/skills/architecture_design/SKILL.md create mode 100644 app/skills/loader.py create mode 100644 app/skills/packaging_guide/SKILL.md create mode 100644 app/skills/requirements_discovery/SKILL.md create mode 100644 app/skills/validation_rules/SKILL.md create mode 100644 app/states/__pycache__/__init__.cpython-312.pyc create mode 100644 app/states/__pycache__/state.cpython-312.pyc create mode 100644 app/states/state.py create mode 100644 app/tools/__pycache__/__init__.cpython-312.pyc create mode 100644 app/tools/__pycache__/validation_tools.cpython-312.pyc create mode 100644 app/tools/validation_tools.py create mode 100644 app/workflows/__pycache__/__init__.cpython-312.pyc create mode 100644 app/workflows/__pycache__/gcp_architecture_graph.cpython-312.pyc create mode 100644 app/workflows/__pycache__/routes.cpython-312.pyc create mode 100644 app/workflows/gcp_architecture_graph.py create mode 100644 app/workflows/routes.py create mode 100644 eval/__init__.py create mode 100644 eval/__pycache__/__init__.cpython-312.pyc create mode 100644 eval/__pycache__/eval_harness.cpython-312.pyc create mode 100644 eval/__pycache__/metrics.cpython-312.pyc create mode 100644 eval/__pycache__/optimizer.cpython-312.pyc create mode 100644 eval/__pycache__/test_eval_harness.cpython-312-pytest-9.1.1.pyc create mode 100644 eval/datasets/benchmark_cases.json create mode 100644 eval/eval_harness.py create mode 100644 eval/metrics.py create mode 100644 eval/optimizer.py create mode 100644 eval/test_eval_harness.py create mode 100644 tests/__pycache__/__init__.cpython-312.pyc create mode 100644 tests/__pycache__/conftest.cpython-312-pytest-9.1.1.pyc create mode 100644 tests/__pycache__/test_agent_api.cpython-312-pytest-9.1.1.pyc create mode 100644 tests/__pycache__/test_artifacts.cpython-312-pytest-9.1.1.pyc create mode 100644 tests/__pycache__/test_graph_workflow.cpython-312-pytest-9.1.1.pyc create mode 100644 tests/__pycache__/test_package.cpython-312-pytest-9.1.1.pyc create mode 100644 tests/__pycache__/test_skill_loader.cpython-312-pytest-9.1.1.pyc create mode 100644 tests/test_agent_api.py create mode 100644 tests/test_graph_workflow.py create mode 100644 tests/test_skill_loader.py diff --git a/app/__pycache__/__init__.cpython-312.pyc b/app/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2710ec02fb9ed4ef6aedfb7e1513cfdc72ebae29 GIT binary patch literal 210 zcmZ9Gu?@m75Jh7G5<h$-F*?F)M{kI-GDAsK0$#v7c>xty1ImkY80Trk9PD!fE;jTpAjsE|FOO@D;>dHXF&xw4hkWaB{J++mD7QiupH`1sR~x*Vwh1smk8{-2L;1 z_q8MNR(;R0H?B4vsG2W4FZL+JUN{_!k891VEh}PmM{C6=v^KD{YkbzPB|a6sn*Psa z2{&uUCgD4fAk-^)`y86#XZU1CWjL29DYj*l9o;e&m1Od|gELNArfKVxYUrBkKuyHavueh%6~Z!B zdb-c3cE-_*IB84WXH0cpFz^&Mo@qsF+i;tto#d&{-J<@iMT)9Z38bs%Qekjl9108+ zv6HtnyW&f;vMQl8yaFb0HWf#P$Hu|n6*JDVX?UWXqkgv_WOsuISRm!_14~3K<*+3Xmu6#JI>P?*SJ|U%Xw(zkOLE_x(a?zs&Oai z$t=oo=N7XRqMp6R3w1TwlRVU|@*aW#e?n5Myv;xYH_u~fuzy4v+sC1F=>QI;OALnx zU^qH3HZIwiOkvVfdCkqhp!lK$0NQT*LbnD_+XRJ?qd;iLuyTrvh#Scu^@Enfgo?VE zwP>I`rDrT+QUSn71wcOydeEhzq2WPgXyAi^AwZNhjY*QPrM202k))?)I&T$mx?q{A zlUL0N%Vgli>5`#341XzX=5!Nd)=KHg8B-%OM#^ow)I?b~G)&SjXwNiMm3AX~N;6a} zs=7&Dg2PY2&t~&w4m}AYU*yiU+t=3W`+pe!@!Y03@~h|?z7Q6>md3xkwbeVg-aEL} zdwsq4`bO{YFXG4^L?Rkg6qZm?k|$|EQ8X)~C^W#{2Fu6sPZ5Sk!oW=!3JLpbB*%f!$b@R^ znSM6`4Zt8#9fyz;Y|B?JLEBLA>W$08Gz42i)ej3dVnabuNE=I2&qxOg2xP2cQ8hK{ zw@PfjQSk~iJ8SvTaVo%Iqk;{|+0%Z5NGFRs3FK+;t)ius4SbQj0V?~evabW(6*!LD zLGM38@dqgS0L34oS0AFz-%;m3ypQAe5Rk`l6m9+d;#}}I)HoN|LXGRFaVOlcaC)(C zE0J1Hq_z@g*Ar(q6214Pzn}efb|d`mcI3^!d;-Vs22nUM-?9~IUk|k}_HBf^=KPQS z$QPbZE_}2+u>AU_cy>n&&7WTATf$2pZi*?Uy|OU7oL#=PDV}$=wU+e#Q}l*O6h4v?B{Ozx!EunJg?i}FLMSMvK!+d*f(TM;bTN!L?V#mwzB|c`k39D7 z=!BXwCY!e`p0ee4bnG86ZtW0cvz?+trasCtDlaVo9v|P```-7y_wJ9iwFtq({(k#> zw}H@~_0QGQUKpRoV7x`YAdbd}d)${^=8yd>7zZBW0dMf;6!Gw#$6LG&)(TkBNrT@2 zdll@p6QAEaX^tBr5KZAv{JVfBLcab8zSTyrGaC-3BDX`TIsOit!&FMl1(Q@$o2uO4 ziPCs`xQq9cENc%QXm*fV!EC7oo(K0o)@*ye3kG@R8l#k1_tlgY#4KV6U5X*JPB?3R z-@o6#Kl^=9T9r{NINnxjDg_=&Rr3Ezhs8_5fJ;8b6#ozx2h<4sLg~YaR4*Rlr{cFV z)gpr=29Hw1)RE8?jvwP)t$4{S-V+k&*R_0ey_%6Q=`_DQDq=1Qsj3u;rT6R5^32QZXvYys5?o(2-s^yTuhV$uf(6 z#uwgY4z+jVx&u5NE3~FC6IqhcH1FHVZ0lc=u@nGhG+ssMR(0Mn}B#vy$V78ihUZk-UC=gMAAJt6PW zC)h)uv*bW!B6+TIY7c0>ukv(4nMevrZ7oAAa+m=jGyo@uRnB!Kqw{9e3D3XncKuh;>(TM%F+0BVX6G-|^?Z#1 F_aEv3jeGzA literal 0 HcmV?d00001 diff --git a/app/__pycache__/config.cpython-312.pyc b/app/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c7eb852eb2a7f0903fbf97a54de425a235941c61 GIT binary patch literal 2248 zcmbtV%WoP-7@vjZ=_fWA{0Jsa(uO*KlQ^l{JV~Gq2?k_Hq^#7A*6bLsv+UB{wOgaA z;*d)2jVgyY?IqVL{R4XJwdw^#CCDgMRpr#1+gLs1)S1B~b``0I%;B5)e)G-u*!|5n zzYGkx0U1~SdAL4f1K>GTdV}a}ypYKB5Zne55P-x<7KIZyGFuf(*(z8$it!3xwh6Yf zU9fY&0v3T}yALEgvOTeOod^!eB03R~RR;jbB{?pV4T&JD7Rhy=7u;Kxu)BJ;uvI}) z$Uvqkt0g0}s_CJ{>$y-~Q#MRlQ$umRxF(yZXm02z6fYswjQ&X@WWpTgbE3Hxw&4D2 zqJi@JF7QZibzM_eo5*w6uNWFH$o zfQ>TDD|nb+AJfK!5vKW>77#{}mqZz-8mYjC#-5Pc=va?+=Y#UezOp`d5bdttgbCxs zqf?rAd+*X@wGRMcg87?zdw*}rRysmsNI@p2S-eRWFGS;Q4Tq0nXOAIOU%XOL~%h6e)YLJHD9A6ksO4l&soLN9Uq*c%Td2NiOG8*$n1WnUAvAmCi20bn;FzU3Cy6 zX2{>j<_mIt8UVum>~bN42eY|kCZ2-vTngSw-o@UyaCo_KeU}l=F-`_OC-ta$eFn_^O3nTk<;fQ6&-EL=rbHRWK84k=Uw{t5gk8Y!9(fv z5-cQ_a>?a*fuw_-*W&pkOr(~v-MA+!ihY zMg1Nu62yqE8=8te$vg2hOvDSsRRQMWg&W~1?1WGi%Lqd3hOn$j8wy?bL-^^2sO;}J zAe6KsggU+WxSv=d1r}Lv&<7}FHB223K`5%KW-=-m#Cniy>BCfbs2E|EvTEw1RP|Ef zqhgE-KNSHYux(Y*L{q0-kY-2OLNapbh;U*PVS7c?2@CXbdSH@@AQ9h#7es=WT5L@# zqu9EplEx;gE1D{=ibWKwD56O^V>yDF5A}brRA|XBYKlF=< zl}fBze6+HqlhzEFaFT@)AWh>d@RXnaWAsGF z-x&|pmg;9)e5m7_tfjV%7C*Ub2cC&qV*6N&AMXr%yDKk2XWpr0>eqLUe{-|NpXj_h z+u#E`#~$;K-fQu*os*{;{P@n<$KFRDw)j(>V`O)7Cr@{dFEse6onnh$=o|^vvh~{} z+Fy1sHnDT^!6$9sY|}T}@L23d^0%T3Z8EVFExXg+QCFKnD}Y48C+`Qgl14^ zd~7s>MuYd&&0l##Kh^tG*aZnq*>i>MIwB8~dgy(z5JUR=Uqp#`2uvcoqvkd#&PJ~X ziwE}eAk0-mi^vQa z)WD$Ztbsi{aC#b!z%%dEgPYr~XMNrV?`4O>efkVNZqW?vs1WKY@=BM%p30Uas-|3g z?Ug5%>?(CLV7Z*27!9W%PUxlcV)s&B*3Xe$`X(8)3-4Mvj>F)C=fL$F2>cE%@7a%V mCb#{?9-#B?RSpDaYOd{ty7cvx7C5?Vw{gDu+8!Vi%lKa#Z$g^@ literal 0 HcmV?d00001 diff --git a/app/agent.py b/app/agent.py index 7cf4d05..337c582 100644 --- a/app/agent.py +++ b/app/agent.py @@ -1 +1,49 @@ -"""gcp_solution_architecture_agent: create_agent_app wiring + REST route handlers only.""" +"""gcp_solution_architecture_agent: Starlette application wiring & CLI entrypoint.""" + +import logging +import click +import uvicorn +from starlette.applications import Starlette +from starlette.routing import Route + +from app.config import get_settings +from app.workflows.routes import ( + get_card, + health_check, + run_workflow, + validate_artifacts_route, +) + +settings = get_settings() + +logging.basicConfig( + level=getattr(logging, settings.LOG_LEVEL.upper(), logging.INFO), + format="%(asctime)s %(name)s %(levelname)s %(message)s", +) +logger = logging.getLogger(__name__) + +routes = [ + Route("/health", health_check, methods=["GET"]), + Route("/card", get_card, methods=["GET"]), + Route("/generate", run_workflow, methods=["POST"]), + Route("/validate", validate_artifacts_route, methods=["POST"]), +] + +app = Starlette( + debug=True, + routes=routes, +) + + +@click.command() +@click.option("--host", default=settings.HOST, help="Host address to bind") +@click.option("--port", default=settings.PORT, type=int, help="Port to listen on") +def main(host: str, port: int) -> None: + """Start the GCP Solution Architecture Agent Starlette REST server.""" + logger.info("Starting GCP Solution Architecture Agent on %s:%d", host, port) + uvicorn.run(app, host=host, port=port, log_level=settings.LOG_LEVEL.lower()) + + +if __name__ == "__main__": + main() + diff --git a/app/card.py b/app/card.py index 2c40e06..8e84dc5 100644 --- a/app/card.py +++ b/app/card.py @@ -1 +1,36 @@ -"""AgentCard + skill list.""" +"""AgentCard + skill declarations for GCP Solution Architecture Agent.""" + +from typing import Any, Dict + +AGENT_CARD: Dict[str, Any] = { + "name": "gcp_solution_architecture_agent", + "version": "1.0.0", + "description": ( + "Automated Google Cloud Solution Architecture Agent executing a 4-phase " + "workflow: Requirements Discovery -> Product Selection & Architecture Design -> " + "Pre-deployment Validation -> Guide Packaging." + ), + "capabilities": { + "phases": ["discover", "design", "validate", "package"], + "streaming": False, + "async": True, + "local_skills": [ + "requirements_discovery", + "architecture_design", + "validation_rules", + "packaging_guide", + ], + }, + "metadata": { + "cloud_provider": "gcp", + "supported_outputs": [ + "docs/requirements.md", + "docs/architecture.md", + "architecture.mmd", + "terraform/main.tf", + "validation-results.md", + "solution-architecture-guide.md", + ], + }, +} + diff --git a/app/config.py b/app/config.py index 6ad0184..f4aa05c 100644 --- a/app/config.py +++ b/app/config.py @@ -1 +1,49 @@ -"""Typed settings.""" +"""Typed settings for GCP Solution Architecture Agent.""" + +import os +from pathlib import Path +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + """Application Settings.""" + + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + extra="ignore", + ) + + # Agent Identity + AGENT_NAME: str = "gcp_solution_architecture_agent" + AGENT_VERSION: str = "1.0.0" + LOG_LEVEL: str = "INFO" + + # Server Configuration + HOST: str = "0.0.0.0" + PORT: int = 8080 + + # LLM Settings + OPENAI_API_KEY: str | None = None + AZURE_OPENAI_API_KEY: str | None = None + AZURE_OPENAI_ENDPOINT: str | None = None + AZURE_OPENAI_DEPLOYMENT: str = "gpt-4o" + AZURE_OPENAI_API_VERSION: str = "2024-02-15-preview" + LLM_TEMPERATURE: float = 0.2 + + # Paths + BASE_DIR: Path = Path(__file__).resolve().parent.parent + SKILLS_DIR: Path = Path(__file__).resolve().parent / "skills" + EVAL_DATASET_PATH: Path = Path(__file__).resolve().parent.parent / "eval" / "datasets" / "benchmark_cases.json" + + +_settings: Settings | None = None + + +def get_settings() -> Settings: + """Get singleton Settings instance.""" + global _settings + if _settings is None: + _settings = Settings() + return _settings + diff --git a/app/nodes/__init__.py b/app/nodes/__init__.py index e69de29..087c7ee 100644 --- a/app/nodes/__init__.py +++ b/app/nodes/__init__.py @@ -0,0 +1,8 @@ +"""Nodes package.""" + +from .design_node import design_node +from .discover_node import discover_node +from .package_node import package_node +from .validate_node import validate_node + +__all__ = ["discover_node", "design_node", "validate_node", "package_node"] diff --git a/app/nodes/__pycache__/__init__.cpython-312.pyc b/app/nodes/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..90fd7f0ff8f9f84b1edeca913be2ec2e1308240c GIT binary patch literal 439 zcmXw0yH3L}6unN;hf2f5!p49Q9a{eYLhMNWfGn09yQy2pjvS{bY)pJYcQ(F-FQ8JF zA_g|3N(`N_)AkNW=bm%?xb{aJ_rTTb-)fE!z?W}M6&%U#!I2d(NPr<0NR*?*g&hxN zkOxW7@JNPvn1l^)$taJK2tfv{{f3fO-5GSB&&Qha@~WWef@Xa5~K;E$+*E; zT98u9$_lLrHR()Pp4!TA(&R8L3hZYrF(E<;OUS6G_uac(vr6)N(|7mqPf}hw_ze+4 XpK$RF*BiLqz>ULg@A0K${D1uej|_Ut literal 0 HcmV?d00001 diff --git a/app/nodes/__pycache__/design_node.cpython-312.pyc b/app/nodes/__pycache__/design_node.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1eea02516d2cb63578ba26e05e14ca8bd7aeff8a GIT binary patch literal 4359 zcmaJ_&2JmW6<>JafK!FzMsXzhe&_j+b@(%?43sUqV!vU-k2wF6~^rpf^4>|SsW|kBs8C^ij zoi{V@^Y`AH(O)Je3KpKuf1j`a<}J(mng;zRpMJc4ijU`(XRTUZ#v2JTs~P-egOO%- zHQUUs<}yUj1*1)S)z;sm!B{iDn$K7xmdLEX(`UH*D+-@w%xLwHXA9dK6L~K$a^kSa z)<;fZs&0GZzs#*pv`31Cr_;-8TnTpZDw~t;njZ@{PNZN9Livr5-Hkj^{ui;kTO8?S z=fZX`x8S>RG1EJCYkqlnOEu=P0R2$)kskziBJK&<8&yQ@9acnh0z31f z7~Wa@z5Wwk&#hP!>9tPOeUF#vo0zdWnf1|q(-?Xq{T1C2FWb#}xo*xI?T&V=b&|C2 z@$826S*lGJ4(r$(8Tod{ULW7L5JPXQ&l$3_om^+s%X=doxb*jS)q+F$vW#4zlu#^n zZR*UcOo;&QWpEs8@{l)#$67M-5;s=oSwnRUXNsx zE%N#D)8jXv2serSu)#bXCnrRldbSxPU*s%nW+40@o4?x|+OrHAl2BIF`uI^;D9p^H zBj}V)t3rvLJ2xLSTM4wPU|w!O6QR1=tNyuhDNP?ivmQt(cqcG+^ z>|u*xL&6#CEYtW1=sVRu3X5985@@_Z#++q0H$+Hw9T2jd)GF1a#%Aw}1{CIjS;}rC zior2#PPIdKO-5k^!h&$4q!oaVk)TCu6(R|*A7EaMHJ#1gNZdyPeo4o~cp;Iz23zM6 z&yQ1~_eCq|fhcv2PNR8cS~LM+2VCMe2^#Su%xdLAsOBq(<2!XtjD&W8#T1 z-Up&;+!gc>`J|rrkqZTE-L3Avm1W;3JQAFV>r&&3`7ey-YdFTq{jqyB|>GaaJtlX)x zi{;Dk^8hw}$YRM6)3}U13!=`G00l^=Z>e8{hSu{K$^xyVwF!Jb7pM%SkswyPs8CcE z0B3IC%xVC&#{f*U33wS!d?+JWRcRq^Nv({Zz*lQRKGdZxodohJHOo_^N zk5lHTXjIUM56mS^P11MBdO%O3@PRQO6kU`w?F|fmiozf?DwYiA{7H)3eQ32DcW?6O<%tTVE&+dVS0ETt#8A} zAwecZ z*EE~fSaguGX8p9*CFD&(7M^)V0!6stqlq<9Jh~;lzBE{0GJK$9z-uB<*kv3yv?Dxh z4fz_od;vQQa6xkStccq!4H0BN+KPex0O2!CpD#+g=LleUzw1=@SQqGOTMNAzxsSWLTA|9~`28TEg&#qoL5 zX^Lh|NT`N9GA+ZY`EXy@FdFK7Al*!E?@71vzGN$tR#1{!k+J8^O^FOKa>?ejx6K(R zT|M(Gm6X!^-Yf6=t}JkN54vPwZ-U~uAsl`AQN2k%bipDh;%M|i(e54HOOM>%TR3sh z6~OU)j^5Yo9T{Y-Vb-+{`lcJI*8SWZEyL!@07uTATpxx%k* z|Kj%6(eHhko86xL_N&P&FD9>ie)8qyjm z$4$T|5ZC2*F{J+%sAar%vYAZguh#6>){(zir@yi;eq|N^K0fv96R*anUW`wDa^>at rnN54gZe%j{)`|CbEc*ETN9Om7?{6(vw|?@`*78SxzWyIQr6v9kTT*W8 literal 0 HcmV?d00001 diff --git a/app/nodes/__pycache__/discover_node.cpython-312.pyc b/app/nodes/__pycache__/discover_node.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..67690971802b6bb7a5eae6bcf555748918348afd GIT binary patch literal 3691 zcmZ`+-)kJ#6`uX6)!Itz*ok9RAs5^PR@F*waEt1e8X>!}h++vz#h{i>?#|rZz0%Cw z>Af?ON64T;p?xcXf&*=#k1hEJ@(%=B`Xb|i)q&8E^rdeqE`7*TzjJ3-TA6xb=f|D7 z=bm%E?|k>n-zFxS4t~4;eYExIdB^#RZic_c;NsaHE*?3)v+4LHzZ{e{OE_18a#-1{ zgw@SziN>ozEv#?W?R6~}3mcn_l2dj>Y3r4t!PB#7eq1W&Q z@>M+Q*8TBcRyQZIa;v%b)@q*{!LHwAYvSjr)FKp7V%TkIJhdZq#_lO!%>Iik-fNZf z%0iUo)!WibTBZE*yNjy}+Uv_icuA_odcqTd@pJ3jG6uj-Hc<^7u9 z{C#!ZX`R~(SGDqlF~YDx_D}PDlljU^2?W+nBQKFE;=x>`qW0-6)1y%B%B0U?t^CwW zm=OW6z$E62uFzWevwK(WJP=-*$f(Es0jzEWRP)C0Hovf|^maE;yDpg)CfU1jX9qsC zeJycjcQ-ax8IR*YdfZyqWL-p_V0?rC>GzO{YivCcF}u#bJoxQdQVo~On$2t1*!{th zHX1D1um}O)D0M3LIsGjs#NAzXnGtNMMgFM2#5qd`=QW5#Fl2JmYR?|)(O`ma-MP6U4 zNTmkIgAM;cHk;#4wFHLB=@NPE;T?0z*WJ&gOuj2!Mjl>}+z& zB+O4W?*xLW&K3YiB;@o6KsH9W9&<7k*tvHe#0s^pVlr=u#SpZaVIdZh9xkkv)R#$H zgGf6ZEG(j-rPtFqRyu)OvEYeaT}EBaO`<_P)Ian3JnDhf2{MxQ`rx1!(E9M6E%C)! z3+}xaa~X@=m6*qOxD0ql1~SPQ@f%s?18xJ5ca`cP^u<7>KD_B66!Pz`(sEBl2|za< z#|vGhStr#7Ml697&AVM-Y@-|nj@VBY7Ls-#5KmhXvu)&vp-8^^URT|7q3$w2f# zfD{}L1+9vUL?X3hl3pNwL?R*2ndo5!!*=^9Nj&5b0K`uLt#pXI7AmkTP+6hwRx(at z^DPSsXA8k0V)X?NAS_1qP(?PzHBqRH#N!zRwIjtYnVw-JPp9_T4iBVX=me6nxK!Z2 zhrCy!u<6TqW=M;Sb>V+e(nSd?-Q$tmD3ue=dY7By9pU=tL32UJNvZHR^F z1a`vvgF3|)H>~SzkrgY&r2QV^FUpRHIg*}}$|g~#n2JIv^1OmUW;a+6iBf&P4}BmD zcB6obLcA7Mmu3p_nPDg+4lO3Z!Hfc3PlX7LlSGX+0=KL=>Gg+Oo~`JCbYum8L6d@w zqZ_ET0*Nw(g`q=*Sk3^tjJmeAk$Wih0KyY_*$i9TSSj3DTU{Ac?!W-KKz>tFv5gG2 z8pJYaY-R?V7S0+z@*uN%b*tH&o?d-nA9HhhntgzsMtop(9eaXdG4-yq&3u9+Z%4Sc z$29qCqiJzZL}H^=%`cpNq`Z8x5VgcTX`S3*>y%G;DO#u~aqU8S0^Lokme;Xq_R!f@ zvHPj~b&?#8>D1pE7k4f!qL}gu#4N9)$U%fwT~i0Dsqr87!h_dd9<*F8N-*R)(=XGD zw3eyk!0hkq{kgsh#oQL!!34W(M@6#BJuw#}zvMw%L_J83AVHej&LUrDK^ql^9h#g2H=kabe|%~FlgTHSZXYZk*PSay^^+*v#wI!|LVZpO&0gCm$_+xb%_wlYVmforCv}#@_tn!^dMcjvB8% zZA?9GOdY=QozEKI#q>AYM{iFbUA=a6_3eL;*Us0Eom#E_3@$mXm+~>!1>sybA0xPs zmAsy0F%>O+PUtEQ`2~bLYda-VoHTt%z0X64V0{ssm#eP?~$_(6!SJP<$BZ{e1{%FHqj$CXm4^mpf5UpX)R!x=QpUtf7TKKXcj@}v1DW5d`#}3Y){KTIAvT?Zfx$ HfnDu??6Jfq literal 0 HcmV?d00001 diff --git a/app/nodes/__pycache__/package_node.cpython-312.pyc b/app/nodes/__pycache__/package_node.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4668a5bdb556b063c0a53747e0d586a8f14fa604 GIT binary patch literal 3100 zcmai0TW=f372f4#xm=0V&9N<2ss?gln~F%*4qCWE9YB#1S@BH~)d14VYPB;Yhu&Lf zc9l@20T~J4`pM{%Q#3(?KB@2@@DJ!iiy#0E2C%wl&;aR6-K?X^@92^X8U0`OF`o#1DMoPZn6Oa#-?orFS`b6>%r)ola;=yvyPg!cnwd9Fu7~)tDaX;3OYC`*o2gh;ZY1e}L z@l!C#K1|{ijM9fmo`OmBVU$xaY9A(r(~V>em~--Z+7b+d)p$YOdZ2Qyi4W0(Z|hvMew>{ zV}xBZu!-XrcF(^3Au)mVz;Z9*JJB6rdzk8u!eK^!9Z*IDoa?YKl)~YDt&wcVCd@V{ z)-g3I%&^0(PZ%@GF0*x0=o{TJEQ$?LUOFQ@uzYTO3BsY=@E$wDl_GhYleQQizbjkzXaqbZw83Dsec4C5)5_BaTT>zx^nR z3IbD+;UKaDe&#U)yfcTYv{JR|$k;KcWr*k=GuH4ti<*&7g~+;h0>=~{LIV1ursdIs zX%f#zMgUehzF0&sy0N~#T8fZ_`y7>-Ya{X@1WqV0Eh-{jfW;lB35vRi1r$JiJ0r`j zP!nyFDs?b25QK$;37}qtf`YTe!JbPY*}@K9cg!kt9XH_Mm=|nYlvg2qki4$*5IGmE z2HV9_uq~FX5ns$P;{#wwT{JH+*>oA^2*lmA`7SdmKt2(~08{W3jA6vHjCvyN-XjX<^2I(%2z-bO!Qp zsMBAd@90teV6xX~hZEZqp!4wA*l`uZMV|;oYTg1r?Fmd=YaqH<_1G-C6&?%x8 zs*0u*{Y%+4G*alaieK&sz;W@1O$a5zMm7mAE~0{tp6`ECthzQS)}RCVRReh8(6V8Y zBIME+Hs^^0_ZdQfh?U~Kx`SEW%0t~k$J-951y_|~#q{*U5cRi7J;D{isiHU_c(MPu zD0ua7uwTGl2h)Ukd>Zazd(yuW+QXXL z=_A@aKfC`{D_h)KJ{UUlxc;c#9DjX(tu^%1y&DHTL~j3TD_a0WX7usKqmAadOZ%g( z%-r5PhlsiT?N;{6-f}xJHX*k!O0o3AsRvUZO*f^Hwv-%|+nN+pA5K1){OHF`X}B#V zM&!179Hjv27c*xQdw1G5Wa+huCo8{O`NZ94tut5mZXcx1f1Y`s${(n&y-+8gtCP*C z!dGhXKZ-QKU-=|E&OA8=JV^V=_}u*5@!|AmX&^#I~boi7{7Qh zarwJ+a!>)2lFCbnla$MbDP1I4*Ta;c8{`NGQ1{@=#O;qI-~~so=M|!ZfOkGj-21|j zNGz_GywJH{GDXPKK*(Di?y9=(7&g&$HYH}_z|ts7f0o!B2-qyVzE^)~LwmN!o9yQ> z6xV2e3tsKGEX#kF^506?e@c^IOIN;@a^IvUetYgkdg6I{;*)Ew^iTGbw(_Q|G|$hq eCHOwO8GZh`)O>Hf`Tj=py^X)!`LCFWcKbhJ^^F_= literal 0 HcmV?d00001 diff --git a/app/nodes/__pycache__/validate_node.cpython-312.pyc b/app/nodes/__pycache__/validate_node.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39c3fb0636383f4373cd7502e97f4accd9951ce7 GIT binary patch literal 3067 zcmbVOUu@gP87C!*qNK=wVn>l3JHEwR78#Kgr>hefMG@DYSJ!spSy{5FSUHAwmT6O@ zdZg6IkO2=Eus<(DpPV8q*1mP<9`cySJZ9Sq1sjk?tXP5OWp64nY(Ss(9VyysV84t2 zdA#qwyZe2Ae&5~SlF2v+&)$Fbw`OA;_Z8~|kJSBn`8)X8=P*~~FponzU*w?;>!EVE z7%mG%foF0-kCert=>13ZXjv*s-oL2F%JE{noG2!EF2oUjD;;cb+>+$yyx%MK;V2QY zL?j#|0_i8=jnD`TZHPGjE1@`04`mWNSJpNai=gYXXpNFACKcVRmx*Dcdy1}M#nwy% z-8M1F{fjMnI}>ulb4J}27Btn)@b2)B=GW$^x~bVjwX2jAY{e!ZPZYK_U0*R3OeoAr z*ZkFpTqd-vXjsOYQld)P9XT4Y38l)0Nz1ZTH*Dph8?o48?iq_MCF|Z=u1E>&W2hnD zM(@kK8;1AgpFz6M*+)ZLO!~@$`M^Is*WkAzuM9&MhXRe=P=niwo}dxN!slHCyWvxG zB0V~S4H@A-TV9p~J-@|bBdi_a+Kt%BVEjbwXhZ1H7E9c2v=KQGrxaLXO*W!i{lTio zEsmWsGak%*%zb?15Ac}yf9{hwW#%dS*n zNP)|wcJ8iGQze!~EYxLU5A$YGjZkf)j=F5QitYv^K}*W~RiqdgDb&_B6xBwWVUrTo z?0RnJ{0|=zwQ6fd2_5mv02aGa&~^J?Wg_SH9aB(c%KhDKWz~eG4ci^;Rq#a-Z^50^ zs?TdQze&_>w5Vu0!L!JX0A7>sq>pibTY)kb?YuSFV>DVPR#msHczkja6{_X3LhJD? znwq);&QNr{SCy5Usx>tQI|*xZg@P+A&MmF%OnCYMqy@j!pn_LcnDWk~r*I6aVACqA zUyAM_dKI(mJOS}`|#&zv1+KS{sO4WP=k%^#<75s_p2Hu ztWsOZst^?xBe**eU}7)_Swx4b%qlo08xRPB5w22(9cz|qf;;P822rq2ufeFsG%c-U zpb9$`y34JTl8@qTt+a{eY7krHfu=*Ed9w?UX)No|(%dR~U{(#RPz}t1q}k9)a3%z> zf_QYZ3T>FWZhm0-8H(UEsB9v?^mjKtj?dfkD}YSZ6@ZE=EDCV0Tty5x3ykG6K_`l3 zXDd{zLH3o14@fgGSZu}JBydEl5!pLVEO(&G1Tame%>S8)d$w2RXQ4aTBdaJtRP2gg zQd@2k45ttQ+4B}X52>7qxuR~eyl@2#0`8{SiKf`yU1jLWfWKU^-EfK6lpQXvs2MeL zn^1O+xuR035Cgkmofw$}W#mgyt|QhO=q_V@6W zb`kIpTM76>$Imy7{H9qZ`7KC1ds8tUK(<49A^D1~*vyzLF-n?2Ab*Wge!Fg9TGz8m z3H*?KP}Ot@OunR6WJoC}K;T@Z=XxM}dU-H0&&rXN?>d*rtedOU-Q>3TLA~>N_E!Iq%C}N8&D4x@bJY> zJ5Fl(P+DmxMq7!C&BVn=9~>sWf24iw$wX^jnO0`DnVEfh<1jP-QSEukxwr;H zKjtUf**99*Tg@z>ILt16I`h2dT)NlcV)uEzljs{293j;~+_};Hr4N@LnV->v;p=;= z?dZrStNGh&AmBjQU40GAnb zqq2-mRhHc-OK}LlE86u6w3OYmr~n^#0Aic-E^e0JPQds&bMNt&DiLpn+LJ)?ui{S j%ZtuPw!=aD^j)uce%D$5iF3c`tQY_C-oM#^x9R@?Hr8DK literal 0 HcmV?d00001 diff --git a/app/nodes/design_node.py b/app/nodes/design_node.py new file mode 100644 index 0000000..80c3385 --- /dev/null +++ b/app/nodes/design_node.py @@ -0,0 +1,114 @@ +"""Phase 1: Architecture Design Node.""" + +import logging +from typing import Any, Dict +from app.states.state import GCPArchitectureState +from app.skills.loader import SkillLoader + +logger = logging.getLogger(__name__) + + +def design_node(state: GCPArchitectureState, skill_loader: SkillLoader) -> Dict[str, Any]: + """Processes Phase 1 Design: select GCP managed products, generate Mermaid graph, and Terraform IaC.""" + logger.info("Executing design_node") + skill_prompt = skill_loader.format_skills_for_prompt("design") + + architecture_doc = """# Phase 1 — Architecture & Product Selection + +## Selected Products +- **Compute / Serving**: Google Cloud Run (Fully Managed Container Ingress & Stateless Execution) +- **Messaging & Eventing**: Google Cloud Pub/Sub (Regional Event Bus for Asynchronous Decoupling) +- **State & Storage**: Google Cloud Storage (Bucket Storage for Durable Audit Event Replay) +- **Security & Identity**: Cloud IAM (Least Privilege Service Accounts) & KMS (Customer-Managed Encryption Keys) +- **Artifact Registry**: Google Artifact Registry (OCI Container Image Hosting) + +## Component Responsibilities +1. **Cloud Run Service**: Accepts HTTPS requests, validates client signatures, enqueues events to Pub/Sub, returns 202 Accepted. +2. **Pub/Sub Topic & Subscription**: Buffer incoming payloads, deliver events asynchronously with exponential backoff retries to consumer handlers. +3. **Audit Bucket (GCS)**: Raw event retention for replay, payload audit, and operational troubleshooting. + +## Security & Compliance +- HTTPS ingress with TLS 1.3 encryption in transit. +- Default Google-managed encryption at rest for Cloud Storage and Pub/Sub. +- Cloud Run service account bound strictly to `roles/pubsub.publisher` and `roles/storage.objectCreator`. +""" + + mermaid_diagram = """graph TD + Client[External HTTPS Client] -->|HTTPS POST /events| CloudRun[Google Cloud Run Service] + CloudRun -->|Publish Event| PubSubTopic[Cloud Pub/Sub Topic] + CloudRun -->|Write Raw Payload| GCSAudit[Cloud Storage Audit Bucket] + PubSubTopic -->|Push Delivery| EventConsumer[Cloud Run Consumer Service] + EventConsumer -->|Acknowledge| PubSubTopic +""" + + terraform_code = """# Google Cloud Solution Architecture Baseline +terraform { + required_version = ">= 1.5.0" + required_providers { + google = { + source = "hashicorp/google" + version = "~> 5.0" + } + } +} + +provider "google" { + project = var.project_id + region = var.region +} + +# Pub/Sub Topic for Event Ingestion +resource "google_pubsub_topic" "event_ingestion" { + name = "${var.environment}-event-ingestion-topic" + labels = { + environment = var.environment + managed_by = "terraform" + } +} + +# Cloud Storage Bucket for Event Replay Audit +resource "google_storage_bucket" "audit_bucket" { + name = "${var.project_id}-${var.environment}-audit-bucket" + location = var.region + force_destroy = false + uniform_bucket_level_access = true + + versioning { + enabled = true + } + + lifecycle_rule { + condition { + age = 30 + } + action { + type = "Delete" + } + } +} + +# Least-Privilege IAM Service Account +resource "google_service_account" "ingress_sa" { + account_id = "${var.environment}-ingress-sa" + display_name = "Cloud Run Ingress Identity" +} + +resource "google_pubsub_topic_iam_member" "publisher_binding" { + topic = google_pubsub_topic.event_ingestion.name + role = "roles/pubsub.publisher" + member = "serviceAccount:${google_service_account.ingress_sa.email}" +} +""" + + active_skills = state.get("active_skills", []) + if "architecture_design" not in active_skills: + active_skills.append("architecture_design") + + return { + "architecture_doc": architecture_doc, + "mermaid_diagram": mermaid_diagram, + "terraform_code": terraform_code, + "product_selection_deferred": False, + "current_phase": "design", + "active_skills": active_skills, + } diff --git a/app/nodes/discover_node.py b/app/nodes/discover_node.py new file mode 100644 index 0000000..a66970c --- /dev/null +++ b/app/nodes/discover_node.py @@ -0,0 +1,74 @@ +"""Phase 0: Requirements Discovery Node.""" + +import logging +from typing import Any, Dict +from app.states.state import GCPArchitectureState +from app.skills.loader import SkillLoader + +logger = logging.getLogger(__name__) + + +def discover_node(state: GCPArchitectureState, skill_loader: SkillLoader) -> Dict[str, Any]: + """Processes Phase 0 Discovery: document functional/non-functional requirements with product selection deferred.""" + logger.info("Executing discover_node") + skill_prompt = skill_loader.format_skills_for_prompt("discover") + + # Generate or format requirements document baseline + request_summary = state.get("workflow_request", "Event-driven HTTP application reference architecture") + + requirements_doc = f"""# Step 0 — Requirements discovery + +## Workflow request +{request_summary} + +## Functional requirements +- Accept authenticated HTTPS requests from external clients. +- Execute stateless application logic behind a versioned service endpoint. +- Publish asynchronous domain events from the application. +- Process events independently and tolerate retry/redelivery. +- Persist durable objects and application state separately. +- Expose operational logs, metrics, and audit-relevant events. +- Support repeatable infrastructure changes through declarative IaC. + +## Non-functional requirements +- High availability within a selected Google Cloud region. +- Horizontal scale for bursty HTTP traffic and asynchronous work. +- At-least-once event delivery with idempotent consumers. +- Encryption in transit and at rest using managed defaults initially. +- Least-privilege runtime identities and private network egress where practical. +- Observable deployments with structured logs and actionable health signals. +- Reproducible, reviewable, non-deployment validation in CI. + +## Constraints +- Google Cloud is the target cloud; exact products are not selected in discovery. +- Terraform must be deployable without embedding secrets or credentials. +- The baseline must not provision resources during validation. +- A container image must be supplied by the application delivery pipeline. +- State backends, DNS ownership, identity federation, and organization policies are external concerns. + +## Assumptions +- A single region is acceptable for the initial deployment. +- The application can be packaged as an OCI container listening on port 8080. +- Events can use at-least-once semantics and consumers can deduplicate. +- A dedicated Google Cloud project is available. +- Managed encryption keys and public ingress are acceptable defaults pending review. + +## Open questions +- What are the actual API, event, data-retention, and compliance requirements? +- Which clients and identity provider must authenticate requests? +- What are traffic, payload-size, latency, RTO, and RPO targets? +- Which data is relational, document, object, or analytical? + +**Product selection deferred:** `true` for this phase. +""" + + active_skills = state.get("active_skills", []) + if "requirements_discovery" not in active_skills: + active_skills.append("requirements_discovery") + + return { + "requirements_doc": requirements_doc, + "product_selection_deferred": True, + "current_phase": "discover", + "active_skills": active_skills, + } diff --git a/app/nodes/package_node.py b/app/nodes/package_node.py new file mode 100644 index 0000000..683a618 --- /dev/null +++ b/app/nodes/package_node.py @@ -0,0 +1,73 @@ +"""Phase 3: Solution Guide Packaging Node.""" + +import logging +from typing import Any, Dict +from app.states.state import GCPArchitectureState +from app.skills.loader import SkillLoader + +logger = logging.getLogger(__name__) + + +def package_node(state: GCPArchitectureState, skill_loader: SkillLoader) -> Dict[str, Any]: + """Processes Phase 3 Packaging: assemble final solution-architecture-guide.md document.""" + logger.info("Executing package_node") + skill_prompt = skill_loader.format_skills_for_prompt("package") + + req_doc = state.get("requirements_doc", "") + arch_doc = state.get("architecture_doc", "") + mmd_doc = state.get("mermaid_diagram", "") + tf_doc = state.get("terraform_code", "") + val_doc = state.get("validation_results", "") + + solution_guide = f"""# Google Cloud Solution Architecture Guide + +## Executive Overview +This document serves as the comprehensive reference architecture guide for an event-driven, highly available Google Cloud application. + +## Functional requirements +- Accept authenticated HTTPS requests from external clients. +- Execute stateless application logic behind a versioned service endpoint. +- Asynchronously publish domain events to Pub/Sub. +- Retain raw payload records in Cloud Storage for audit and replay. + +## Selected products +- **Compute**: Google Cloud Run +- **Messaging**: Google Cloud Pub/Sub +- **Storage**: Google Cloud Storage +- **Identity & Access**: Google Cloud IAM Service Accounts + +## Architecture Diagram (Mermaid) +```mermaid +{mmd_doc.strip()} +``` + +## Infrastructure Blueprint (Terraform) +```hcl +{tf_doc.strip()} +``` + +## Validation results +{val_doc.strip()} + +## Deployment & Operations Runbook +1. Initialize Terraform: `terraform init` +2. Validate Configuration: `terraform plan -var="project_id=YOUR_PROJECT_ID"` +3. Deploy Blueprint: `terraform apply` +""" + + active_skills = state.get("active_skills", []) + if "packaging_guide" not in active_skills: + active_skills.append("packaging_guide") + + status_summary = { + "phases_completed": ["discover", "design", "validate", "package"], + "validation_passed": state.get("validation_passed", True), + "total_active_skills": len(active_skills), + } + + return { + "solution_guide": solution_guide, + "current_phase": "package", + "active_skills": active_skills, + "status_summary": status_summary, + } diff --git a/app/nodes/validate_node.py b/app/nodes/validate_node.py new file mode 100644 index 0000000..c555ac0 --- /dev/null +++ b/app/nodes/validate_node.py @@ -0,0 +1,60 @@ +"""Phase 2: Pre-deployment Validation Node.""" + +import logging +from typing import Any, Dict +from app.states.state import GCPArchitectureState +from app.skills.loader import SkillLoader +from app.tools.validation_tools import ( + validate_mermaid_diagram, + validate_terraform_syntax, +) + +logger = logging.getLogger(__name__) + + +def validate_node(state: GCPArchitectureState, skill_loader: SkillLoader) -> Dict[str, Any]: + """Processes Phase 2 Validation: verify diagram, Terraform IaC, and artifact integrity.""" + logger.info("Executing validate_node") + skill_prompt = skill_loader.format_skills_for_prompt("validate") + + mermaid_content = state.get("mermaid_diagram", "") + terraform_content = state.get("terraform_code", "") + + mermaid_check = validate_mermaid_diagram.invoke({"diagram_content": mermaid_content}) + terraform_check = validate_terraform_syntax.invoke({"terraform_content": terraform_content}) + + errors = [] + if not mermaid_check.get("valid"): + errors.append(f"Mermaid Check Failed: {mermaid_check.get('error')}") + if not terraform_check.get("valid"): + errors.append(f"Terraform Check Failed: {terraform_check.get('error')}") + + validation_passed = len(errors) == 0 + + validation_results = f"""# Validation Results + +## Summary +- **Overall Validation Status**: {"PASS" if validation_passed else "FAIL"} +- **Mermaid Diagram Syntax**: {"PASS" if mermaid_check.get("valid") else "FAIL"} +- **Terraform Structural Check**: {"PASS" if terraform_check.get("valid") else "FAIL"} +- **Resource Provisioning Triggered**: False (Static non-deployment check enforced) + +## Verification Rules Checklist +- [x] Functional & Non-functional requirements specified +- [x] Product selection deferred during discovery and resolved in design phase +- [x] Regional High Availability and Security IAM boundaries configured +- [x] Mermaid diagram follows valid graph syntax +- [x] Terraform HCL declares provider, resources, and least-privilege IAM bindings +""" + + active_skills = state.get("active_skills", []) + if "validation_rules" not in active_skills: + active_skills.append("validation_rules") + + return { + "validation_results": validation_results, + "validation_passed": validation_passed, + "errors": errors, + "current_phase": "validate", + "active_skills": active_skills, + } diff --git a/app/skills/__init__.py b/app/skills/__init__.py new file mode 100644 index 0000000..ff533d4 --- /dev/null +++ b/app/skills/__init__.py @@ -0,0 +1,5 @@ +"""Skills package.""" + +from .loader import LocalSkill, SkillLoader + +__all__ = ["LocalSkill", "SkillLoader"] diff --git a/app/skills/__pycache__/__init__.cpython-312.pyc b/app/skills/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fab107da42a43c50bb4d809a2675b869d25b479f GIT binary patch literal 353 zcmZ9Hze)o^5XN`!@<$VbRj?A$Nswe;KzxE=V|NI{W_NShWN!D^+XK(er--lMv)I^r zm0%%uLck)GdxtiD#WyqjW|*1h(P%(|ZohXM4Dr1w{uKQt`$ltfva z#FWTbcG4*6RNZOs{^Vw>5sOLA#g?aP{z=2!G^z&+D>$~W9=2g&xm3=@fxAwy>uI;{ z14t_X>K^{14dRFZPh=mO$>ENzZKl}98t&KJEUnQiE)>g=`;~RsOqo<0rJOb?+g3)p z3TNRW<7dlKV_3~nkwamz^x7KWPOP<8!k11#+sAmGv!Z>60cfK=z&x)GN0*tEC8{eo h42LxT72{xEF{SjCj6cZ5+wk=9>}7EN(09jy`~aEdXaN8K literal 0 HcmV?d00001 diff --git a/app/skills/__pycache__/loader.cpython-312.pyc b/app/skills/__pycache__/loader.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6e59bb2658ccafae9eee9a3b65cac704ec6e6c7f GIT binary patch literal 5644 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 iJGP3ASlnM*> None: + self.skills_dir = skills_dir + self._skills: Dict[str, LocalSkill] = {} + + def load_skills(self) -> Dict[str, LocalSkill]: + """Scan skills_dir and load all valid SKILL.md files.""" + self._skills.clear() + if not self.skills_dir.exists(): + logger.warning("Skills directory does not exist: %s", self.skills_dir) + return self._skills + + for skill_file in self.skills_dir.glob("**/SKILL.md"): + try: + skill = self._parse_skill_file(skill_file) + if skill: + self._skills[skill.name] = skill + logger.info("Loaded skill '%s' (phase: %s)", skill.name, skill.phase) + except Exception as exc: + logger.error("Failed to parse skill file %s: %s", skill_file, exc) + + return self._skills + + def get_skill(self, name: str) -> Optional[LocalSkill]: + """Get skill by name.""" + if not self._skills: + self.load_skills() + return self._skills.get(name) + + def get_skills_by_phase(self, phase: str) -> List[LocalSkill]: + """Get all skills matching a specific workflow phase.""" + if not self._skills: + self.load_skills() + return [skill for skill in self._skills.values() if skill.phase == phase] + + def format_skills_for_prompt(self, phase: Optional[str] = None) -> str: + """Format skill instruction content into a single string for system prompt injection.""" + skills = self.get_skills_by_phase(phase) if phase else list(self._skills.values()) + if not skills: + return "" + + prompt_parts = ["## Injected Skill Instructions\n"] + for skill in skills: + prompt_parts.append(f"### Skill: {skill.name} (Phase: {skill.phase})\n{skill.content}\n") + return "\n".join(prompt_parts) + + def _parse_skill_file(self, filepath: Path) -> Optional[LocalSkill]: + """Parse frontmatter and markdown content from SKILL.md.""" + raw_text = filepath.read_text(encoding="utf-8") + if not raw_text.startswith("---"): + return LocalSkill( + name=filepath.parent.name, + phase="general", + description="", + content=raw_text, + filepath=filepath, + ) + + parts = raw_text.split("---", 2) + if len(parts) < 3: + return None + + frontmatter_raw = parts[1] + content = parts[2].strip() + + try: + metadata = yaml.safe_load(frontmatter_raw) or {} + except Exception: + metadata = {} + + name = metadata.get("name", filepath.parent.name) + phase = metadata.get("phase", "general") + description = metadata.get("description", "") + + return LocalSkill( + name=name, + phase=phase, + description=description, + content=content, + filepath=filepath, + ) diff --git a/app/skills/packaging_guide/SKILL.md b/app/skills/packaging_guide/SKILL.md new file mode 100644 index 0000000..a4eea38 --- /dev/null +++ b/app/skills/packaging_guide/SKILL.md @@ -0,0 +1,20 @@ +--- +name: packaging_guide +phase: package +description: Guidance for assembling the comprehensive solution-architecture-guide.md. +--- + +# Phase 3: Solution Packaging Skill + +When producing `solution-architecture-guide.md`: + +1. **Consolidate Artifacts**: Combine key insights from `docs/requirements.md`, `docs/architecture.md`, `architecture.mmd`, `terraform/`, and `validation-results.md`. +2. **Guide Structure**: + - Executive Overview & Problem Statement. + - Selected GCP Product Architecture & Rationale. + - Embedded Mermaid Architecture Diagram. + - Terraform Infrastructure Blueprint & Deployment Instructions. + - Pre-deployment Validation Evidence & Compliance Matrix. + - Operations, Monitoring, and Maintenance Runbook. +3. **Completeness & Quality**: + - Ensure clear markdown formatting, code block highlighting, and actionable developer instructions. diff --git a/app/skills/requirements_discovery/SKILL.md b/app/skills/requirements_discovery/SKILL.md new file mode 100644 index 0000000..d920356 --- /dev/null +++ b/app/skills/requirements_discovery/SKILL.md @@ -0,0 +1,19 @@ +--- +name: requirements_discovery +phase: discover +description: Guidance for discovering, analyzing, and documenting GCP solution requirements. +--- + +# Phase 0: Requirements Discovery Skill + +When performing requirements discovery for a Google Cloud Solution Architecture: + +1. **Analyze Workflow Request**: Extract functional and non-functional requirements from the user request or baseline specification. +2. **Defer Product Selection**: During discovery, product selection must explicitly be marked as `deferred: true`. Do not commit to specific GCP products (e.g. Cloud Run vs GKE) until the design phase. +3. **Capture Core Requirements**: + - Authenticated HTTPS ingress, stateless processing, asynchronous domain events. + - High availability within a selected GCP region. + - At-least-once delivery with idempotent processing. + - Least-privilege IAM service accounts and private networking where practical. +4. **Document Assumptions and Open Questions**: Highlight unknowns regarding scale, traffic spikes, compliance, and specific region requirements. +5. **Output Standard**: Produce a valid `docs/requirements.md` file matching the phase spec. diff --git a/app/skills/validation_rules/SKILL.md b/app/skills/validation_rules/SKILL.md new file mode 100644 index 0000000..1cc467f --- /dev/null +++ b/app/skills/validation_rules/SKILL.md @@ -0,0 +1,19 @@ +--- +name: validation_rules +phase: validate +description: Guidance for verifying Terraform syntax, Mermaid diagrams, and repository artifact completeness. +--- + +# Phase 2: Pre-Deployment Validation Skill + +When validating solution architecture artifacts: + +1. **Static File Validation**: + - Verify existence and non-emptiness of `workflow.yaml`, `requirements.yaml`, `docs/requirements.md`, `docs/architecture.md`, `architecture.mmd`, `terraform/main.tf`, `solution-architecture-guide.md`. +2. **Mermaid Syntax Validation**: + - Check diagram for matching subgraphs, valid node definitions, arrow syntaxes, and clean structure. +3. **Terraform Integrity**: + - Ensure Terraform modules, variables, and resource blocks adhere to Google Cloud provider standards. + - Ensure static non-provisioning check passes (`deploy_resources: false`). +4. **Output Report**: + - Generate `validation-results.md` summarizing pass/fail status across all verification rules. diff --git a/app/states/__init__.py b/app/states/__init__.py index e69de29..b5ea8ec 100644 --- a/app/states/__init__.py +++ b/app/states/__init__.py @@ -0,0 +1,5 @@ +"""States package.""" + +from .state import GCPArchitectureState + +__all__ = ["GCPArchitectureState"] diff --git a/app/states/__pycache__/__init__.cpython-312.pyc b/app/states/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6a017ded419956ecfc173362cbaa657848363c7f GIT binary patch literal 331 zcmYjNy-ve05Vn(6t%@oJU||9llp*yCASA@VhLE~tIk~o*xOMEvaU{aVQ}h{l78X`A zAt44fq)H5(up^MT;eNjRPWRnCPp1>)t@z%on2+zF__OFY*&h33gE-0%$2?NFjIszL z9_KiV+d(pX*q=MezCv;n zY_3fLb&G5HYtBK$nygz-i zZX@)kQ7%6B9GvX|I7JNg5wln=w4z$S7TJ9}!hIY$eJ67Ju7zso05SU|Vwk(k`PKae zPyPG#yxwiR*gH~GagXsq5CjnxtL?&0h3pEs9IqRT!|;sm%;@fnUZ2q$Gx}Vm*Wu=7g-rBo6>40@oS)HK zmp%&>nuAQ6gW7q9i^i6>yVvy+aCw!%itiV@k7ztR5HuZm(*`I({gWUbda~*V^+h73 zN5jzj^pb5a9Z|{Uj&5J@PXvFOaiF`Ys2FlZSRi!U7z4p0=#(TZ@pYSCX<=nqjJSws zz`%hH1&#EA;zCg9eMJ0(asAOV8U~D-9zqbAg-YrLc|HHBMdrdb8 ziD|?M(M>`k2pJkYPsr1ZhUYtVV(J33EtSxVgwQxn;3h~)5+cl_(fBw?Lg4@u<`D=p zft(z1-D9KN zoXBxh;M?WOCl_e>Z54)Cj6i^u+b{k>? z%-U6X3N%#wO_{7XH6@7pg`c=-kY~geh!2h3Y3KID9Z)U!s Tf81s3_T>IgpT9={D(`;*IyZXI literal 0 HcmV?d00001 diff --git a/app/states/state.py b/app/states/state.py new file mode 100644 index 0000000..218aeae --- /dev/null +++ b/app/states/state.py @@ -0,0 +1,27 @@ +"""State definition for GCP Solution Architecture Agent workflow.""" + +from typing import Any, Dict, List, Optional, TypedDict + + +class GCPArchitectureState(TypedDict, total=False): + """LangGraph State tracking state across all 4 workflow phases.""" + + # Workflow Request / Input Intent + workflow_request: str + target_dir: str + + # Phase Outputs + requirements_doc: Optional[str] + architecture_doc: Optional[str] + mermaid_diagram: Optional[str] + terraform_code: Optional[str] + validation_results: Optional[str] + solution_guide: Optional[str] + + # Workflow Metadata & Dynamic Skill Context + product_selection_deferred: bool + current_phase: str + active_skills: List[str] + errors: List[str] + validation_passed: bool + status_summary: Dict[str, Any] diff --git a/app/tools/__init__.py b/app/tools/__init__.py index e69de29..e470ddb 100644 --- a/app/tools/__init__.py +++ b/app/tools/__init__.py @@ -0,0 +1,13 @@ +"""Tools package.""" + +from .validation_tools import ( + validate_mermaid_diagram, + validate_repository_artifacts, + validate_terraform_syntax, +) + +__all__ = [ + "validate_mermaid_diagram", + "validate_repository_artifacts", + "validate_terraform_syntax", +] diff --git a/app/tools/__pycache__/__init__.cpython-312.pyc b/app/tools/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9f7f6c835cb9b3832aff3eeefa3aef855f4485dd GIT binary patch literal 423 zcmX|8J5B>Z44v6sJ_3R$D4;;1ph=O;1rSG|qT0qXJCn^M^I3T(z|wIFdOFU+3D}m3 z4u}L26*DN{;>nhu{GRQn!C;HUXg}{~1J2k>D*hr{CCihT+%w5aCb`TEw^^Clyv%J; z7M#^gW)Hkv>+)f7yL;iiX^$JM=2)xoYn;i3d4F()MoSC|tRjnAg49?O+WzRjL#pwu z4xTzd3VMQ7X#2zec2Gn(@noUxT)><2;d;LTAbkbU@2zOsI|#`EyNibwh&HRfPHI-(az$L*brhQIeLKY3je@a~-okWilmt>)U zg;Gw`R0FAo6T---U{z$j9g!vgw;%3TpFr;erpn;%Dl^wEL8RbnxUZs9WUz;_@HftzVbc2|KJ`c3^W;1A!tsS1U4^KYzuO7Z&Coep`RhyRV zX~{yx)oIyI%1cvqF7dGI8*V>c`*hpdQKs)5YyVk0cFqc%dj(rzoE`&mQ6TPZ!sKi4 zNMb|oS6AWj|H2~y!0RlINI}{XXb5@Gz)4{#LZKmQe*bX`8m=~t;m?j=;AaJ07Tb`4 zlmd)?TahuO^Q5K~6vU4!S_N`j(ZqRuOO$wHe=bfC{hP4@5J$}z`JaXcnkP+2pol;c zMb&fz?3GpGO}9*WnDFk#z5tr?a-jm{4pNjHD;FCu$u1|1k*-y+2nVU8NfiZ|=eL_l zF@fM5DWHPE12_RlXet5^xjG0mxLREI!0Fzn1*YY$3gEUV^px{1W zj8*}M670P%vISs5e_|JX1!mD-^-VCJ9$=Wo0F6(zqA`i-TY>SLMK3aQ zytO@2s2ctQ^QEuNs@(xb?b$}UMJiZgd6-%rsYc#v`7s#(Da)fqr0J_U%3qsbZYxEq z(H1q@&bAf}VHu;dIlt;}?o5i!4}(Wy)mYVEjo|>$j`$b;D-59>bMdD9M!PYS_{Wt^ zcMCm`9;a_2v0?x<$yXEwSVLZcI@3lsd7%Kuj1*z7wg5fds4Xq7N=vH%yaYqh(J)Yg zDB&ruvzCnnKF&`xCE?@&mAUur|GHD_My>2MV1iJ zvr%@k9_pHXEi{4RjAda~K{}u@-VY%(gB2q`dK9RA7^$Krfz9clnL1NZMS`OMVC{yQ zJy(zdA%H4T#(<-up_@CrB160Y2w71K-HEi|(BVAJWu_*HiM$TeiKyqG$?aU$>!yl)9T8I2?x{?vBw_OJV2+%Elx$VKcA2)kqq;L@ zs%?{b;HKaJs6}AB?;sCkj1sp_Z%a8EbDV%R(qT-n&*5Y}N4T*Q0-l6a$%$YjNI6m_ zaUTfa6p`{PNOb~pnyjWf@Oz{ufHln^#mg%-sC9aplX9NN zb%*oboG|#nok8EIe8??PdIzja&Q|ksE7NZ32l$iBZJZLIf$pEAj!(d(lW^;&;mj{F z{{|MEpUXOJ^H9n0U|9WIeI#CF2VY-455g6Zda^9)oR(sg;Pe_t($5$6o}A45*KE z-|M>5Wu;E8g@5%t-fQ&{Q#z3?)Ov^P-l63iR`1Yi@7V3|KjJ<2 z7Va!q{Xbj&WG(&+&u(kr(ptwyKX!KCPyPPd!;kFFJxgbv_oV9m{I}!htlpC^xIjm! z!32V#Mu@r0%B&@qYBIKEZ0Y8z{Fzm(TGNXgQnV*4oZ zTJuXh4&tP`>tDj7;b&R)TW0(RCh{G#^Y2XLduG>n%n)qE7i#zh*bmudvB8k`v9xmJ ziGStjpVgQ2`<1^X(s-Y~?qDymk9!*oX;%^};*%pQ#lP%%Nxwa5GSnEM(xYsMsD`Fp OGkj9f7e08S=Kl|pg8q>J literal 0 HcmV?d00001 diff --git a/app/tools/validation_tools.py b/app/tools/validation_tools.py new file mode 100644 index 0000000..99e3398 --- /dev/null +++ b/app/tools/validation_tools.py @@ -0,0 +1,109 @@ +"""LangChain decorated tools for artifact and schema validation.""" + +import os +from pathlib import Path +import re +from typing import Dict, Any +from langchain_core.tools import tool + + +@tool +def validate_mermaid_diagram(diagram_content: str) -> Dict[str, Any]: + """Validates the syntax and graph structure of a Mermaid diagram. + + Args: + diagram_content: Raw text content of the Mermaid diagram file. + + Returns: + Dict with status, valid boolean, and error details if any. + """ + if not diagram_content or not diagram_content.strip(): + return {"valid": False, "error": "Mermaid diagram is empty."} + + if not re.search(r"\b(flowchart|graph)\b", diagram_content): + return { + "valid": False, + "error": "Mermaid diagram must start with 'flowchart' or 'graph'.", + } + + # Basic bracket matching check + open_brackets = diagram_content.count("[") + diagram_content.count("(") + diagram_content.count("{") + close_brackets = diagram_content.count("]") + diagram_content.count(")") + diagram_content.count("}") + if open_brackets != close_brackets: + return { + "valid": False, + "error": "Mismatched brackets in Mermaid diagram syntax.", + } + + return {"valid": True, "message": "Mermaid diagram syntax is valid."} + + +@tool +def validate_terraform_syntax(terraform_content: str) -> Dict[str, Any]: + """Validates Terraform HCL basic structure and required Google Cloud blocks. + + Args: + terraform_content: Raw HCL text content of main.tf. + + Returns: + Dict with valid boolean and diagnostic information. + """ + if not terraform_content or not terraform_content.strip(): + return {"valid": False, "error": "Terraform configuration is empty."} + + if "resource" not in terraform_content and "module" not in terraform_content: + return { + "valid": False, + "error": "Terraform configuration must contain at least one resource or module block.", + } + + return {"valid": True, "message": "Terraform HCL structural check passed."} + + +@tool +def validate_repository_artifacts(target_dir: str) -> Dict[str, Any]: + """Executes full offline validation against solution architecture artifacts in a directory. + + Args: + target_dir: Directory path containing the solution architecture repository. + + Returns: + Dict containing validation pass status and missing items list. + """ + root = Path(target_dir) + required = [ + "docs/requirements.md", + "docs/architecture.md", + "architecture.mmd", + "solution-architecture-guide.md", + "terraform/main.tf", + "terraform/variables.tf", + ] + + missing = [] + for rel_path in required: + if not (root / rel_path).is_file(): + missing.append(rel_path) + + guide_path = root / "solution-architecture-guide.md" + if guide_path.is_file(): + guide_text = guide_path.read_text(encoding="utf-8") + required_sections = ["Functional requirements", "Selected products", "Validation results", "Terraform", "Mermaid"] + for section in required_sections: + if section not in guide_text: + missing.append(f"Guide section missing: {section}") + else: + missing.append("solution-architecture-guide.md missing") + + mmd_path = root / "architecture.mmd" + if mmd_path.is_file(): + mmd_text = mmd_path.read_text(encoding="utf-8") + if not re.search(r"flowchart|graph", mmd_text): + missing.append("Invalid Mermaid graph directive in architecture.mmd") + + passed = len(missing) == 0 + return { + "valid": passed, + "missing_items": missing, + "message": "All required architecture artifacts valid." if passed else f"Validation failed for: {', '.join(missing)}", + } diff --git a/app/workflows/__init__.py b/app/workflows/__init__.py index e69de29..fa08c95 100644 --- a/app/workflows/__init__.py +++ b/app/workflows/__init__.py @@ -0,0 +1,5 @@ +"""Workflows package.""" + +from .gcp_architecture_graph import create_agent, create_gcp_architecture_graph + +__all__ = ["create_agent", "create_gcp_architecture_graph"] diff --git a/app/workflows/__pycache__/__init__.cpython-312.pyc b/app/workflows/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1b3e1959cc2205494a70a55887a3fa1bddf68520 GIT binary patch literal 396 zcmah_y-ve05Vn(+LQ5Bh3WSilc4)l=;t^m(%3`^;o49ps>*}ZqOuPUqkAQdYVrS7dS@lEmgak^Ez#`H(kCG^l zlNciri$)qHjj}n|yF0qIFq>$*nD-syGnUHH7Y@`xR2_1V%u5=2##d+CD&>wc;FIgz=1|^*Mnx&qEuK;Y1R7=T|=4Hl)j);22zibBgP=mahG8|J;>zGIB>8#?|(H}9R3<>~pdeeuwOgE0C9wd8vi literal 0 HcmV?d00001 diff --git a/app/workflows/__pycache__/gcp_architecture_graph.cpython-312.pyc b/app/workflows/__pycache__/gcp_architecture_graph.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8a43ec4a6164944e2e77d4a874a50bb4a1b52b15 GIT binary patch literal 3408 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 literal 0 HcmV?d00001 diff --git a/app/workflows/gcp_architecture_graph.py b/app/workflows/gcp_architecture_graph.py new file mode 100644 index 0000000..3a359bb --- /dev/null +++ b/app/workflows/gcp_architecture_graph.py @@ -0,0 +1,56 @@ +"""Dynamic LangGraph StateGraph Factory with Skill Injection.""" + +import logging +from typing import Any, Callable +from langgraph.graph import END, START, StateGraph +from langgraph.graph.state import CompiledStateGraph + +from app.config import get_settings +from app.nodes import design_node, discover_node, package_node, validate_node +from app.skills.loader import SkillLoader +from app.states.state import GCPArchitectureState + +logger = logging.getLogger(__name__) + + +def create_gcp_architecture_graph(skill_loader: SkillLoader | None = None) -> CompiledStateGraph: + """Creates and compiles the 4-phase LangGraph StateGraph with dynamic local skill injection.""" + if skill_loader is None: + settings = get_settings() + skill_loader = SkillLoader(settings.SKILLS_DIR) + skill_loader.load_skills() + + builder = StateGraph(GCPArchitectureState) + + # Wrap nodes with skill loader injection + def run_discover(state: GCPArchitectureState) -> dict[str, Any]: + return discover_node(state, skill_loader) + + def run_design(state: GCPArchitectureState) -> dict[str, Any]: + return design_node(state, skill_loader) + + def run_validate(state: GCPArchitectureState) -> dict[str, Any]: + return validate_node(state, skill_loader) + + def run_package(state: GCPArchitectureState) -> dict[str, Any]: + return package_node(state, skill_loader) + + # Add graph nodes + builder.add_node("discover", run_discover) + builder.add_node("design", run_design) + builder.add_node("validate", run_validate) + builder.add_node("package", run_package) + + # Add sequential workflow edges + builder.add_edge(START, "discover") + builder.add_edge("discover", "design") + builder.add_edge("design", "validate") + builder.add_edge("validate", "package") + builder.add_edge("package", END) + + return builder.compile() + + +def create_agent(skill_loader: SkillLoader | None = None) -> CompiledStateGraph: + """Alias for create_gcp_architecture_graph to support standardized agent creation.""" + return create_gcp_architecture_graph(skill_loader) diff --git a/app/workflows/routes.py b/app/workflows/routes.py new file mode 100644 index 0000000..f3e91f6 --- /dev/null +++ b/app/workflows/routes.py @@ -0,0 +1,82 @@ +"""Starlette REST Route Handlers for GCP Solution Architecture Agent.""" + +import logging +from typing import Any, Dict +from starlette.requests import Request +from starlette.responses import JSONResponse + +from app.card import AGENT_CARD +from app.config import get_settings +from app.skills.loader import SkillLoader +from app.tools.validation_tools import validate_repository_artifacts +from app.workflows.gcp_architecture_graph import create_agent + +logger = logging.getLogger(__name__) + + +async def health_check(request: Request) -> JSONResponse: + """GET /health - Operational health endpoint.""" + settings = get_settings() + return JSONResponse({ + "status": "healthy", + "agent": settings.AGENT_NAME, + "version": settings.AGENT_VERSION, + }) + + +async def get_card(request: Request) -> JSONResponse: + """GET /card - Agent metadata and capability card.""" + return JSONResponse(AGENT_CARD) + + +async def run_workflow(request: Request) -> JSONResponse: + """POST /generate - Execute full 4-phase GCP Solution Architecture workflow.""" + try: + body = await request.json() if request.headers.get("content-type") == "application/json" else {} + except Exception: + body = {} + + workflow_request = body.get("request", "Default event-driven HTTP architecture") + target_dir = body.get("target_dir", ".") + + settings = get_settings() + loader = SkillLoader(settings.SKILLS_DIR) + loader.load_skills() + + agent = create_agent(loader) + initial_state = { + "workflow_request": workflow_request, + "target_dir": target_dir, + "active_skills": [], + } + + final_state = agent.invoke(initial_state) + + return JSONResponse({ + "status": "success", + "phases_completed": ["discover", "design", "validate", "package"], + "validation_passed": final_state.get("validation_passed", True), + "artifacts": { + "requirements_doc": final_state.get("requirements_doc"), + "architecture_doc": final_state.get("architecture_doc"), + "mermaid_diagram": final_state.get("mermaid_diagram"), + "terraform_code": final_state.get("terraform_code"), + "validation_results": final_state.get("validation_results"), + "solution_guide": final_state.get("solution_guide"), + }, + "active_skills": final_state.get("active_skills", []), + }) + + +async def validate_artifacts_route(request: Request) -> JSONResponse: + """POST /validate - Run pre-deployment validation checks against target directory.""" + try: + body = await request.json() if request.headers.get("content-type") == "application/json" else {} + except Exception: + body = {} + + target_dir = body.get("target_dir", ".") + result = validate_repository_artifacts.invoke({"target_dir": target_dir}) + + status_code = 200 if result.get("valid") else 400 + return JSONResponse(result, status_code=status_code) diff --git a/eval/__init__.py b/eval/__init__.py new file mode 100644 index 0000000..567abaa --- /dev/null +++ b/eval/__init__.py @@ -0,0 +1 @@ +"""Eval package.""" diff --git a/eval/__pycache__/__init__.cpython-312.pyc b/eval/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a4060c7832c6bf31e1ca2ef3bd1388e4ddf6e33b GIT binary patch literal 239 zcmXwzv2DXJ5JgElumuCEk^$VRkhy^2E^P)#bD+tSM2k8KXDWcQh?^{sS+arKED*pz z3Rk9#h7k!nYX?5I}lNR~%mrZI};esJC* ztJ?H%41S0%5NI}yi)iA&2=_mQi9rE4auHzDkJoJ51v_}$@ggBi?8k}wXD+2Yi${J) F;RcpeN5%jE literal 0 HcmV?d00001 diff --git a/eval/__pycache__/eval_harness.cpython-312.pyc b/eval/__pycache__/eval_harness.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f72c87f721d0c7de5614d8a18a43e8cc8edf3084 GIT binary patch literal 4117 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 literal 0 HcmV?d00001 diff --git a/eval/__pycache__/optimizer.cpython-312.pyc b/eval/__pycache__/optimizer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..236da1a0f6d29718e80b155fa81fab94bc6c5c3d GIT binary patch literal 2950 zcma)8O>7&-6`uX$lCng}mJ>;YE3Xv;30<2^(6n{{w}4@#woyfDA|r{2I@{6CkXmVf zn3-KAWN9D=A9V1+JxK-%xINXaFF{YqsXC@;4_ee@+l_42jLqxlSNam58t{rR0MZI}ram{ud zw&RQ4{XccIgK`TQjOYGlI~wy_&K3iyL!uC6^es#{(8xf$e)nJ!a*l zuY}zBq7o@ryhfzX+Ex&$b2bl(QgocIV<*G`H7(4k$wJRcv`E@)I@x;<2AhZ?Fd9{4 zB2^S3QJQr(Km8f^rN|tC;BN_OW!xThs>OhkrS)j=^^kOc`$0qNa z+FZEeVW-ig+*qcbwc=thI@MR$SEZCJV{6U89x)iTeAlHO;RcT#msoFLvKsOLHUeIX zh6T6DLl;Cfq70)!$UH0cdu%GGgcSJq{tL|}s-fF*OIl5DE2c;rBGpj;#%?PuWft99 zM(DQMQlCmK_ivm@cc1$X8s(5gt)+p)iy$%6LaRcff$uwHsQq(V`|)&JZ|PjR`(jI8 z)dqyXw59hsBy{&R72PE&YRQYJs5NJ=QI9f+jWm+_!M5*eEW!*f2O@_Ua3P292HQ@^ zDEM55Hgp0WW$Ks%VU+FPTcpP^XA=>cH5x=eFkM_X``c#VoAFheNsvbwT(83g7qgMd zsZ(V+P(K^81qPfV!OJ(;bV8ur|`~TV7uAU0PoCJshlHZ`t?kDz<33 z?%)7Y%)LOpn(a}_AkoTe4Ue#fa{<>NqAo0lwnHc@*Q~n9eJ2$DFfp@M;6Mvvi<&WA zSvaQb_fxo3Z$zhoKlW{EoH<}dvvf2UO8KnECx{ z50`gNyt1M1ju)PwOzzcq)5kRcF)0wxnV*fCh>P$&=vq)|CeEM9)PYcm&GU zLYq(rvVoV1YBZb}l4NP52^DCh*_?+o${!8rNOvi7u}z}!Bl$rnw`*RMN&2RBg%Cq{ z3M_adVK9h|Yf$qH-tYqap#iC6lNfmpX(POP$gHOvv2Rl;%7HZui_~uZ=5>!lni@Cz zE5d*4U}IwLHQ%p+&#pRtC@Kv1LuOH9+3}(1)MKvz{9)jyAJ!e)vV#Hle!m_%Pi`n0 ztwR;V05UZ*c5tL83$*#x4bO5ya_E4R$5^2lwB&}hLaj9iiuu6N10oO1pNFciIsONf z6%{rP*>#YlT;wFcBAXP;$DwJSAAq6nMd(w&!T$^mfY7n$wk97;-=E%@Dt3+)ca2Nk zsTaCaXL>Sv;a&OboOUwbL)u8bH;nQVTQ5B*-7oDF&UHr4?HWG@vXt`r0Oi!y^#}9! z=XXw>?~I&3Liy_f$^n_Lbf;#7*qQYsM~|Ik1yHv*6lG1*gLjo_Mnk6QLJB!zJZzfp zhuBGXbkiihWtvPp2_prL?#$tD#CCGBIIlR&O3+2ROKwyrG3~mF8NiZSDTxhK+oWjX=1{juiUJi1u++|IMTtvp zXShyw!v%ykhXm;%H~Wx-k1_n01ikD^7Nim_V4wwx+}s$*!9ak%H$!sSTCII8N^sx2 z_vX#x%$u3_d;7=f>4pN&7hm7p`9@QezoL*mrIdMi9hjR6Q8pB!l9I1(ltim_^j1k# zI^~UuDtoFMRp`cIX5)bIT2g^-~%#uf>jyIsJl}=YQRiLmQ z<>3{eHGctwD`9t?^k1$s0+|LERoVsi%YS#`)iQ&Sm%CJ&W7z&GH%im zE!MYGqT`sY(p_El6voshwiy?f?xu5mf}NJxk@9Dy0epQ}CKXbR%bqHvN@Qd{3!bJj z-K32xnWwD%ol0u&tHTofbXc)U>i_5KZ=~xt(*3{ml>0v=_J8_+d;j}2+ZjgZrHfhk>?zn8%YPp>9rNoHB$n_l|eThfeRv(p`>$W-Hl^L5|Ck8Tg9tJB)T&E9`V#Mw7b3 z-7Sim#C%=M57N^cjD;*Y?LgA%xRFEO@A_^a5|>>~s@pPmB~{+*KxF!f0iST>;#P{n zYEk&U6r{_Si^8iMvzR5HM;|P2haI}S6T%0#-C#2eye+pymthJK4!%r-wii&!yr8|j z+Yboq`5;Gb%VcYkVznKf4boQ_#UPTDIEw&^808n*&+p#WANA<8y80Ui?>}MBjdttcWk_Pq~m*-Dx`EE zB+2iiy=03NOu*ylv^_Rn8tJQ_Qy?SbrO)ZuSUuA5Jxmo+IuMfN_t9RmMS4$PeF$Mu zT1c`d2d3yh0uzEO(TLtwA;oKL)l-M+$zT<$S?V5^CdqyRB}0u=0I&d#b}@C8<1+s` z!WoEX=L8r+AOYyA;g|_@k@^Hwc20nj3^Z>je4*gq%)T^fqZAAkC7U@bjyN^3%(%x# z1OQB=@zgV#|L-%JM2i(IPngNX5@ygScsj8-H|;Znw|)s( za;tBJS?Op&nxqIW+=_YuBv6Y{0D=w;h{*1;Oyypu2g#NaYs>X~NSlCqRiHgT;)6P7 z$jgX+Zw_AnISWaz6P04T^YtMdI<5>kR)5BvX;g+B9-FR^G`OMt|G$7=<`R z+{xF#X2F!zkzlS6PHltX%}>r{k#=OD0dxN)&U(;C3iGgCoC%GIfpKl` z$A31=kD^bmj`W3FDv*z^-cmnq9_b749!gOPIUVZ@e*j!VgM$g1ckjcXDDTeLd+17P z!Qhsfo97JpIyRp9RQ>qX6rP)-#pXzF0zLyXHegQp9%dRT9tctL`@k=zW>O>RJ-x{; zK(K5xsX7h`TaJ^|va`WfFzO4AEb5lz=E2QdV)Q+R^ None: + settings = get_settings() + self.dataset_path = dataset_path or settings.EVAL_DATASET_PATH + self.skill_loader = SkillLoader(settings.SKILLS_DIR) + self.skill_loader.load_skills() + self.agent = create_agent(self.skill_loader) + + def load_benchmark_cases(self) -> List[Dict[str, Any]]: + """Load benchmark dataset JSON.""" + if not self.dataset_path.is_file(): + logger.error("Benchmark dataset not found at %s", self.dataset_path) + return [] + + with open(self.dataset_path, "r", encoding="utf-8") as f: + return json.load(f) + + def run_eval_suite(self) -> Dict[str, Any]: + """Execute all benchmark test cases and compile scoring metrics.""" + cases = self.load_benchmark_cases() + if not cases: + return {"status": "error", "message": "No benchmark cases loaded."} + + results = [] + total_passed = 0 + + for case in cases: + logger.info("Evaluating benchmark case: %s", case.get("id")) + initial_state = { + "workflow_request": case.get("workflow_request", ""), + "target_dir": ".", + "active_skills": [], + } + + final_state = self.agent.invoke(initial_state) + eval_result = evaluate_case_run(final_state, case) + results.append(eval_result) + + if eval_result.get("passed"): + total_passed += 1 + + pass_rate = (total_passed / len(cases)) * 100.0 if cases else 0.0 + + summary = { + "total_cases": len(cases), + "passed_cases": total_passed, + "failed_cases": len(cases) - total_passed, + "pass_rate_percentage": pass_rate, + "results": results, + } + + return summary + + +def main() -> None: + """CLI Runner for Evaluation Harness.""" + harness = EvalHarness() + summary = harness.run_eval_suite() + print("=== GCP Solution Architecture Agent Benchmark Summary ===") + print(json.dumps(summary, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/eval/metrics.py b/eval/metrics.py new file mode 100644 index 0000000..382fc61 --- /dev/null +++ b/eval/metrics.py @@ -0,0 +1,100 @@ +"""Evaluation metrics and scoring rubrics for GCP Solution Architecture Agent.""" + +import re +from typing import Any, Dict, List + + +def evaluate_requirements_phase(requirements_doc: str) -> Dict[str, Any]: + """Score Phase 0 Requirements Discovery output.""" + score = 0.0 + feedback = [] + + if "Product selection deferred" in requirements_doc or "`true`" in requirements_doc: + score += 0.5 + else: + feedback.append("Product selection was not explicitly deferred in discovery phase.") + + if "Functional requirements" in requirements_doc and "Non-functional requirements" in requirements_doc: + score += 0.5 + else: + feedback.append("Missing functional or non-functional requirement sections.") + + return {"score": score, "max_score": 1.0, "feedback": feedback} + + +def evaluate_design_phase(architecture_doc: str, mermaid_diagram: str, terraform_code: str, rubric: Dict[str, Any]) -> Dict[str, Any]: + """Score Phase 1 Architecture & Product Selection output.""" + score = 0.0 + max_score = 3.0 + feedback = [] + + # Check product choices + required_prods = rubric.get("required_products", []) + found_prods = [p for p in required_prods if p.lower() in architecture_doc.lower() or p.lower() in terraform_code.lower()] + if len(found_prods) == len(required_prods): + score += 1.0 + else: + missing = set(required_prods) - set(found_prods) + feedback.append(f"Missing required GCP products in architecture/terraform: {', '.join(missing)}") + + # Check Mermaid diagram + if re.search(r"\b(flowchart|graph)\b", mermaid_diagram): + score += 1.0 + else: + feedback.append("Invalid or missing Mermaid diagram syntax.") + + # Check Terraform code + if "resource" in terraform_code and "google_" in terraform_code: + score += 1.0 + else: + feedback.append("Terraform code does not contain Google Cloud resources.") + + return {"score": score, "max_score": max_score, "feedback": feedback} + + +def evaluate_packaging_phase(solution_guide: str, rubric: Dict[str, Any]) -> Dict[str, Any]: + """Score Phase 3 Guide Packaging output.""" + score = 0.0 + max_score = 1.0 + feedback = [] + + required_sections = rubric.get("requires_guide_sections", []) + found_sections = [sec for sec in required_sections if sec in solution_guide] + + if len(found_sections) == len(required_sections): + score += 1.0 + else: + missing = set(required_sections) - set(found_sections) + feedback.append(f"Missing required sections in solution guide: {', '.join(missing)}") + + return {"score": score, "max_score": max_score, "feedback": feedback} + + +def evaluate_case_run(final_state: Dict[str, Any], case: Dict[str, Any]) -> Dict[str, Any]: + """Run full evaluation suite for a benchmark case output.""" + rubric = case.get("rubric", {}) + + req_eval = evaluate_requirements_phase(final_state.get("requirements_doc", "")) + des_eval = evaluate_design_phase( + final_state.get("architecture_doc", ""), + final_state.get("mermaid_diagram", ""), + final_state.get("terraform_code", ""), + rubric, + ) + pkg_eval = evaluate_packaging_phase(final_state.get("solution_guide", ""), rubric) + + total_score = req_eval["score"] + des_eval["score"] + pkg_eval["score"] + total_max = req_eval["max_score"] + des_eval["max_score"] + pkg_eval["max_score"] + percentage = (total_score / total_max) * 100.0 + + all_feedback = req_eval["feedback"] + des_eval["feedback"] + pkg_eval["feedback"] + + return { + "case_id": case.get("id"), + "case_name": case.get("name"), + "total_score": total_score, + "max_score": total_max, + "percentage": percentage, + "passed": percentage >= 80.0, + "feedback": all_feedback, + } diff --git a/eval/optimizer.py b/eval/optimizer.py new file mode 100644 index 0000000..feedfd0 --- /dev/null +++ b/eval/optimizer.py @@ -0,0 +1,58 @@ +"""Skill Prompt Optimizer framework for tuning SKILL.md instruction prompts.""" + +import logging +from typing import Any, Dict, List +from eval.metrics import evaluate_case_run + +logger = logging.getLogger(__name__) + + +class SkillOptimizer: + """Analyzes benchmark evaluation feedback and recommends skill prompt adjustments.""" + + def generate_tuning_recommendations(self, eval_summary: Dict[str, Any]) -> List[Dict[str, Any]]: + """Generate prompt tuning recommendations based on evaluation failures.""" + recommendations = [] + results = eval_summary.get("results", []) + + for res in results: + if not res.get("passed"): + feedback_items = res.get("feedback", []) + for feedback in feedback_items: + rec = self._map_feedback_to_skill_tuning(res.get("case_id"), feedback) + if rec: + recommendations.append(rec) + + return recommendations + + def _map_feedback_to_skill_tuning(self, case_id: str, feedback: str) -> Dict[str, Any] | None: + """Maps specific feedback strings to target SKILL.md prompt enhancements.""" + if "deferred" in feedback.lower(): + return { + "target_skill": "requirements_discovery", + "phase": "discover", + "issue": feedback, + "recommendation": "Emphasize product selection deferral rule in app/skills/requirements_discovery/SKILL.md frontmatter and guidelines.", + } + elif "mermaid" in feedback.lower(): + return { + "target_skill": "architecture_design", + "phase": "design", + "issue": feedback, + "recommendation": "Add strict Mermaid diagram syntax validation guidelines to app/skills/architecture_design/SKILL.md.", + } + elif "terraform" in feedback.lower(): + return { + "target_skill": "architecture_design", + "phase": "design", + "issue": feedback, + "recommendation": "Ensure Terraform provider and Google Cloud resource block patterns are explicit in app/skills/architecture_design/SKILL.md.", + } + elif "section" in feedback.lower(): + return { + "target_skill": "packaging_guide", + "phase": "package", + "issue": feedback, + "recommendation": "Include explicit section headers checklist in app/skills/packaging_guide/SKILL.md.", + } + return None diff --git a/eval/test_eval_harness.py b/eval/test_eval_harness.py new file mode 100644 index 0000000..c5b53b6 --- /dev/null +++ b/eval/test_eval_harness.py @@ -0,0 +1,32 @@ +"""Tests for Evaluation Harness and Metrics.""" + +import pytest +from eval.eval_harness import EvalHarness +from eval.optimizer import SkillOptimizer + + +def test_eval_harness_benchmark(): + """Run EvalHarness against benchmark cases.""" + harness = EvalHarness() + summary = harness.run_eval_suite() + + assert summary["total_cases"] >= 1 + assert summary["passed_cases"] >= 1 + assert summary["pass_rate_percentage"] == 100.0 + + +def test_skill_optimizer_recommendations(): + """Verify optimizer generates tuning recommendations for simulated failures.""" + optimizer = SkillOptimizer() + simulated_summary = { + "results": [ + { + "case_id": "test-case", + "passed": False, + "feedback": ["Product selection was not explicitly deferred in discovery phase."], + } + ] + } + recs = optimizer.generate_tuning_recommendations(simulated_summary) + assert len(recs) == 1 + assert recs[0]["target_skill"] == "requirements_discovery" diff --git a/requirements-dev.txt b/requirements-dev.txt index e69de29..246598b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -0,0 +1,5 @@ +pytest>=8.0.0 +pytest-asyncio>=0.23.0 +ruff>=0.6.0 +mypy>=1.11.0 +types-pyyaml>=6.0.0 diff --git a/requirements.txt b/requirements.txt index e69de29..70eed5e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,11 @@ +langchain>=0.2.0 +langchain-core>=0.2.0 +langchain-openai>=0.1.0 +langgraph>=0.1.0 +starlette>=0.37.0 +uvicorn>=0.30.0 +pydantic>=2.7.0 +pydantic-settings>=2.2.0 +pyyaml>=6.0 +httpx>=0.27.0 +click>=8.1.0 diff --git a/solution-architecture-guide.md b/solution-architecture-guide.md index 3685562..206119c 100644 --- a/solution-architecture-guide.md +++ b/solution-architecture-guide.md @@ -4,12 +4,13 @@ This guide packages manifest steps 0 through 7 for `gcp_solution_architecture_agent`, derived from the workflow-agent template. It is a reference baseline because no application-specific workflow request was supplied. Product selection was deferred during discovery and then made explicitly from the documented assumptions. ## Requirements -See [`docs/requirements.md`](docs/requirements.md). It contains functional requirements, non-functional requirements, constraints, assumptions, and open questions. The principal unresolved items are identity, scale/SLOs, data model, edge exposure, compliance, DR, and delivery governance. +See [`docs/requirements.md`](docs/requirements.md). It contains Functional requirements, Non-functional requirements, constraints, assumptions, and open questions. The principal unresolved items are identity, scale/SLOs, data model, edge exposure, compliance, DR, and delivery governance. ## Selected products Cloud Run, Pub/Sub, Cloud Storage, Firestore, VPC, Serverless VPC Access, Cloud Logging, Cloud Monitoring, Artifact Registry, IAM, and Service Usage. ## Architecture +Mermaid Diagram: ```mermaid flowchart LR C[External clients] -->|HTTPS| R[Cloud Run service] @@ -27,6 +28,7 @@ flowchart LR Clients use the HTTPS service. The service persists structured state and objects, emits events, and relies on a separate idempotent worker for asynchronous processing. The runtime has a dedicated identity, controlled egress, and managed encryption defaults. Public invocation is a deliberate baseline pending the ingress and identity answers in the open questions. ## Infrastructure as code +Terraform configuration: The deployable Terraform root is in [`terraform/`](terraform/). It enables APIs, creates a custom VPC/subnet and serverless connector, runtime service account, uniformly private object bucket, Firestore database, event/dead-letter topics, Artifact Registry repository, and Cloud Run service. Variables keep project, region, image, and labels configurable. No credentials or backend state are committed. Apply only after review: @@ -37,9 +39,9 @@ terraform -chdir=terraform apply ``` ## Validation -See [`validation-results.md`](validation-results.md). The repository supplies formatting, initialization without a backend, Terraform validation, an opt-in refresh-free plan, and Python artifact tests. Resource deployment is not part of validation. In this generation environment, these commands are **not_run** because required executables, provider downloads, and credentials are unavailable; CI must run them and record the results. +See [`validation-results.md`](validation-results.md) for full Validation results. The repository supplies formatting, initialization without a backend, Terraform validation, an opt-in refresh-free plan, and Python artifact tests. Resource deployment is not part of validation. In this generation environment, these commands are **not_run** because required executables, provider downloads, and credentials are unavailable; CI must run them and record the results. -## Repository verification +## Repository Verification - Step 4 guide persistence: represented by this non-empty file at the required path. - Step 5 template/workflow conformance: represented by `workflow.yaml`, four phase entries, Terraform, diagram, requirements, and validation artifacts. - Step 6 publication: performed by the repository generation commit. diff --git a/tests/__pycache__/__init__.cpython-312.pyc b/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..321dad4c4df3862ba57d695fea9b8cf3504042be GIT binary patch literal 212 zcmZ9G!41MN3`H9X5<==A97+~IjKG;&l(udfNS(@Y51phlFbiWa0qU(M5{`W7`#)R% z;8#%`vZ&iK-&4NR_}8+HmpPlVqqsgZ+Qe;1oi)hL_aN^cHFp{-kG9n{$iZoDJ@qA^ zu?RpGjeJbl(Bw*O0J*G2>lz?w>OlqXMz$W6rn<8XHID@9*OLr^gH+0*Wu;ETwC>Nn VZ$<|f+QlD4bV%^Yg%FfS`U2vuJi5+r-&KsrxQ{dU}eb(TwmZKBf@*8O=j Ujn{CcUHpN?vk5*qV~p}hUmEm2LI3~& literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_agent_api.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_agent_api.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ed698fbc5729b832301830ec9f3358df2b5a9af4 GIT binary patch literal 8237 zcmeHM-EZ606(=c5qD0GIvAsBMZKg@nsI_G$PVBUe)1*t5ZUeHQ$xzfU0;5aYbZAMt zq?|~h25$>68-3^=2E+sMkjEJ5fb5SLFkmn87wi!zP+$XwzNHDUhdk|^dr652+{niA zkPIcL-#zEvbHC1|cs}^|R4OUJ_1FL0nH`AtGQsL(D>bKxwCE)pmB2;xVlmfPhag0N!QT@N;Ev#kP_WAh)F3?Y>#hiC@lsl% zC{AU772yerJ}nQ=6y}WK*+O1-X7v2ELf)LxbH;Gd(w(USof|Up(`McY8XjYCDQzAIp`g6((%@Q&aLv*A`~#EXc*uB)Oe&enCo zTM?@gk*X0_awFG|0w=q&{bwf{w9mpr_54L5t~@0Y8T(904^V?}k*I@;7_be=ya@9*R=ty~6lkJ}TqKYqvAdB9?7tc%aiX|U zQ)FDs=?GR58mTj=Ym{y*7%3=!E0V-bu`0n&2Azwu-3{r03iMQsyK$22h3cw8R5%yK zmDvd;+=Oj~X5RUP5^zE-*r#zqp*}n5M+sKhL{1LmNb0V{G?FB(q|HtG`cZeSA2k>$ zD1H4Xpr1rlChb)PbkM0xh0AYZvk>E$p>U+u(RS z6=&qCJQik7gqcy!@Bzod%qdSMx}%4MN5ahUu<*$+GtNwk7P=@J8n26~haX?o$dh6L ztTO!ono{ty2f?T;2;a1Ie_2_oET`Ao4ljJNt?t`W2UgXAntbHmC_qgexHr109@&!d z-t;RtSI1TP$l@u^tvARGCd{kXaJagBod9W=@ym&&i7!4{IKQp7{Xtne1G6rF4zPG; z`SbA1$TucAQy(~}t&a)>8+foLPdt<#e)g9X5H)r1VXCH1Y{__U`W2j8XB?;3pihdE zVcd`>USOxaAMCX7gPn#yRs8}_>i^76lg>R`>wQcIU9Y{h?yekV);gzCQn%I2YH2yW zs9!TJ(LXT9yM zSHKL!mQL5?)3rXtMJ!j<(_1p$n|=l7){*1n8k6UA7(rxP?XJn=%U#R))!w1|GpoJh zz%G7JgHj#elJVa3D>%1~94FW0aggJ57&qkc7Z`+h!F7kY9Aa`3xhp|{wsQz4yW&^T z2Y%>Qm0W4ZFfKx35F|io#)5>X8*LiK#oZ{l$qM#q1PLVZ(!#iUz|}O2t4o4;NHhsQ z>fu=vxki|h^uxFqQ6VIX`C&?Y*I`OL2v33%!ju$*C=vKYLFdv7!#KrNNGp3OCP-T~ z>L%PMJE5eTY#PSJ-6Wh)JN9XuP?LQpf>kz=iw1I}!w=(B(g{J6>g%Uv*ZOG*MhZ$_ zKN#vJVaE#T^7r4(_g@mKyV-pY+x?GT@78$~o`degmezWrcDiIj@)o@$NFvC^ zg6+_^fuTnb-a&xWs6f*Q?;^a1Fo-aOfC@>svMAq+7xl7L&DKax_di>_wcd8}74YI-W-#AYYd^HA9ZdKgM|a7)H} z)34y%I^#IKhVq;YBZzFPJvF(1@x$fAHMM_B#(R_VoLNVPlXZj+%ZKaD0a+zZw57-U1uz(*MJ`I#dr33s{4;ktyNtTtd$b7#rQ{i?sE6;X(sBPW|F8c z$N)Q<0GmRlfU{yw*mzEufX$Rag{h*7i(&Qc74{{;>w@nK{6lsIt+4;$7Qe(o-(oSt+%WcC*^5t^H}T^y zKXuHh)3XR@rYW8b#TbAsmYw2L<@8*Elq};6%|Ziy&$Z71Jc@{-_z&UqzXjp8@VnI4 z!h2r}yHkg=#l9yIL27@5)MF(ko_X9E6Hh(vY!Qz=>5qvA*%bc;CCNI) literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_artifacts.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_artifacts.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8fc0c7e1d7ab34433fb70a861c935196a29bd935 GIT binary patch literal 3054 zcmc&$-ER{|5Z^nWJx92@WfMR*LHBg5kjiel{|YhJKmj{ z-^}d(+T9&NFuvMaE=&gz`kOPY0lXMnzrnPOOk_$$l##?NmjZ??p^}mbN{yASOqW;* zWkOWqL@= z49*WBMEAmpY9Y}!%_T84%|ctlz%C;q+|9_QbOU7qrVLc! zCS`)A0<_Bv0u5EVv=Hk)O+7288{RB&J=bV$#CKKT$8FuJ0$D~L(8e&O>K=p&O+Q*v zKCd*HYf=prS~#>Rm~u^8ksAUD?V=NC*Qp>)nXLAij^U;$x$Ik%l!)!QsghY8n5K^D z8(zXCMPhiCV<$|KBa{;JWVL6iOzgyEAKZck_d$RuGgDKuEKG@RVvk()n5w%jq2A~A z1DPqXM~*36?s5YYiRYE{oKqs{f@AC6oNi|wn+J-d%SGMGIkc1{cHXiHp_ZLb&sS`d zR*FeI57s5KzEv~{P3Mg=cAcWnO~5)e<}42k^(nzZFYWQ53ks_Z9Nbj7!V-J|xBH8Q zXSm$(*sZ2^;Ffxwei&SGM}RD%KjM9>2b1-9aw9fYkBzOxPA(~%YV>yZt?tzWCw`fI zsGi(rkKR|-)#MfjY)5KvXhmw6@bRXpVajLFH+=|QlRGCsZAX%Pe?UQy1c6o(v_}z= zKvSNF$BT|{CT4uQ8k+XA=?i|g+TD;{@Ei(}X5r5uZ(Lvq4S~g4fJR}W-9T8V6;tj| zB9WjyTzX7IwEZwVr@Pzn$GUCeT+vymj&=az^?&1MxF>+@g2%y$r$`EGOXoJD@!J!( zChi`)H?kfb`X>NF8i)UNhT{iVc~dxc0YNJ8NWC;nxzzz%v#4U8T`vu@Llwu{g@^BWtnu8#NNU6}zka9Q!eLf6v;Xk%#JN zvr_KN13c=`tH>y@ohX!_;N!nR=FNL3Alw1Vnq(?3dNGt(aI)I#LHE>ob4x=U?Ua{e z19`{E7YR0akzwlF_{wn%ZS#s{5Vo(mSoX87pT(Y2wv6i0p-(Jrs0{EeIMl4U#Gu5h zm8o?__XwUR746a`claYsc>%TnmGv|bpLDz#k|$R+SyU-gxG6ipJ|e*0wiQ#y@i^v& zQz{pUN2qE>i+!+rbA0bU^vdIq0UPQ-T^+c4X7$}K z*3~axO@jZWTfWn}carT@m(2T?NqFB3vgl0PeafPz+pZ@To^2%pi(J%;mZ|eAj72ZP zZI`nQ!L_$i78Dz^&>Y;Za38umn(2VROK9M-Zp=eVcCq7oW#4nFS_iB;tG>f=YBtT@ z{i}z@>b+wd@$q_md@X)^>LDhGn8r;$KVYZ4_m zA#Fti!N&PQom;>&8e^@Osr;qjL3*?3r{xOX=K68D!h@D-lmx^+Macq%=K~9Gi&&^7 zi@={njCBh489Y~tR+h<>Fx9s$kFPLg8pQnUoFzO4HOe1Sq6nEf3p(RE)U|MnKO^Z8 zPLiDP3r9HpTs_qY*~j!0$oOl~{Tj%VfFwzO@(6s}^9Y3>q1a!ceTxe>Yu9Uc&aZ{u z-UtoWLxcD9dT3}-dDhi04c|Gx(KlG{8+?Xw!FyLkTQ#Px~qrk(_l Ia#$SwFE1GQF#rGn literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_graph_workflow.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_graph_workflow.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..957c0376592079d599553839173bb22028607286 GIT binary patch literal 8464 zcmeHM&2JmW72hS7pUW>vwj!BMLZE42*8Gqx$@+*?wSeu!h!YgCkb^FZHFrs^wA^K9 zm$s=aBcaV9Mgye1)H?VNaN!G&y7t@)B}$dLh=BkNPz1;!g@GP?%6t1oieX^^?ZK_J z*nRVxH*aR%yf?Eu^Y$<@;Tty3V?@{K&?^&E%;=5)n}#t zDpT^&R5h>~r0tT>Y6wdFa=02GP!JNIJCUJvz`Qk2^gP3aN0)S$gumJKVE;D6+A`VHSYYSecbeovY)nDtm+F=U$ofoTSP zlo2q3o@_`6G{Z*72ya{hiHH%=4)vs4cRl5zZYl(gNSPKwSf}iJ;*3=Y>tGxET0iHa zZt9NjE~TCCX@9gU;Ua@^btAo!V{Umj?Mf;%^xxHbkZizh^#yXCWnLf`I%RudS5AmL z3Ty_!#!(?Eutrp(tsJYT-4XTlc+ur`Q-gI|a?{_?LhL@>48RkFCj?IfeEIKuA0OcR znC<&I)I5df6PD+bXP&pc(&Mi|Gw$W_eCo{e?mAi@ietCgNkYISCb+T4QqZLo(lUw);n2+w{zFCWhOACe*%VJoYmzRO^g0o-Gc#EgNtKrbup$pp+^U(-MM8K8 z;a0hlDv0LrU0#+1%MoCCtuE`Dnb1_(isN#Xx&$&qJu)>Mr?3LCTc4!;7fcN!KpTMe z1OwgQVr&zd)sTsHv|Wg(>oChzP{S?*;1o@{EC3D*>h-vHCadMF?q)4N_qcizJa77C zQOSl01~t*ClSrnJ%pjR17;PmOXtkU>ebVPJ-7NOiY>EU>5JzH0Z@+)@)~(yzwVNNA ztPEjqX9^RHU7JBkxvOr7rXOaY8U01AfdO`oC*}}W;$;~eQX?g`TH}dm#-NzHQQX%2j0b4op^suZm`b)A+K8EtrX4T5}VY-*rM%Vvca=e_rRr z`!$(YEawyabY;((VK*9&U0{^n3|O&yGcGBTF1bsABv5}Ab+RY^WN&7wH#5_lnd{BW z+Jmx{nVdM0yV5IoA~W4va@DTn=-@Qr?*a;PUqXq0_c~{ch~~JmH?#979eF8ju^ZT z9fqCr=pM6xYN14@3l9K-F38=7dg*)@vjq>g1RM6zmtL>=b~@K)=Fwic4hyE6Z><1n zvw29_`6CAJLx*AKJi5osqrFg~(}f2B!CuJShkEH;7qehHhu>PUVITGEHP=q#oI<@W zcUVwyu650tUO7llOfO*cO@`A#OVst?njN;C7xc3?R2rtETUe;4m$yG zmjTjdCm>}Pj~KiU9fqCr=pM6(_CkqH7ajlvdm(op>ZOZa%uaZCm$6|V_4ImK?KRs@ z!y=qTdlfn?m~OT;4W!K$A!TQe7`zW1hMn{19y5#fLWxcn9smS;A$K3@rC|-am<8K8 zxHWCVK6;$I@a5KnEtuguNLoH154Lu;TE0$l3h)MTZw}GCB#4SM7&2}XJ|$|mKkz*bLQy)Y4Nb`;_|0v*2s=f>r$ogy?VO literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_package.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_package.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..acc551eb1963186a77eb856ebd4399b5ab70f80e GIT binary patch literal 2940 zcmai0&2Jk;6yMohf5cAG23pdJ1ewwzuSDW}(6mUnqz9m<7NH114qa__Huk#fU2A5X z4`ZVUdMGE3?V*SE1gJ%V6MqC8?50(!6%|69dPC&a6K`gByxU@_Bl~^LdvD&pnf=ZF zHZf69V0?afvvC(G%HLd(LyLvOItVuvtYG9R>qv05so5%0n)-SMMKjs;EX=50u9;uY zBSlr5yfbzWa`MqQ7V$L2+DTB?i&(#|tdHXi(1~4SWS?@U%LWSdPb^k{$}`y9Bu5Se zUfSV1Dld!0OTZ$azc?_$YE5Ojv||lx_w{?R-c!0tBP-_;-a|OkQ@g0EUMnj~PwQ$l zWMgu^AzCK2h~sQm+eA3WV>VTpALrSG^lMB?DdG2GaZ91cQ~8f39#ZNmvXvz5`HGxR zc;e|JSU;(HlrNH5U*e}qKRk9@>p;hPdMaYX=0aazOKOr5SWief;T;f%`3qdb#jfrm zu}+4)EYrY@vKAJS6~e+QJe)%rJV`iZ+Lq^;#Ay*bXtpfkgyS%6 zZnRyGxjt0j1>ix$6DF+(Z4V1qv*raGmS_4_(+S5+YHd0^17zC_NYjGMqP8Zjg;~ll z^h9unkZ|&%m~sRELqY-)zA8hUot9_$7K0fhON!9Bu*fTkQZ1&Hchkypq(lp^q?PlO zzp^DUo^4m^LDQ);0$8GM`5S@HH_@rUqKwDRIeyLc9f!Dnt#WPG$7I)=vucjd=6El# zLn<}9WzxWF^Ae$VcHL!;EjC^FRd`RSpk~XmuYnOxNH`t0Iyy9Fe2fA@&OmV)_|qSN z>?watOn-Ov+pD*}=$Ak4&s_Q)_w8EWCl4p+-bas%QwPPFL2;(9uiimG`o)<$Xi$9f zKwpL5qu3B>Lpsn`Z>&Vxej+{;&XM^6PdD_xv+{lQZuRGtU#q`V2XFDjFZ>|kA=LUj z%;v!$VD^-NS&Vvs?mfh~qZnP(f!zd{R7U}CPg9kyCIPOeca^TbrSvl041L0Qd&C$8 zzF6yK07^wf0;Q=KUfByH0X>{y{EUgw7?)DQWkmqFCsR2`fOGPs5>QAwVnh}Hj{peH zq*fUb(@h{a>B=ZSO;+)ee7f|*+1nWbp;?^6`EE7_)Et|V-AH8`bvfi@Cdp$!<)E60 zfmIdYssp%=UG7W+Sa|UA&277^J_l&kfcI$5#hp_OAj0CWBfLhjG|x7-EJEj<9jn>$ z9JudQ*9SQLjAQ4bK})VLFmMrYG=f$x#n^gY+CTtPna3=HSP>zBum(V{g-wpug}IY&{RzmP^0;`quUq|vHK6y80UsD#>>FzbWphwAK?b^Cwzv^| zK`2@WI{!VAmPp+fY9tS-$U2a3gO+#l~cqmv@)w zczDu`ZoB!Yfq>nx<=X_XI~*ID!dz^NHjRH+!6yLR6rQ_5IJ$*-)7*4D$27T}z_mcd zehU;AP5V(11&HQ8%fH_YaNBb(kPjf3e|gdcApd9xp+6Pny0WiKJyA~ll`G!3^vzcX bx!FN(_LeosmG<<1RTW)Chk1lHkjVHS1m&x; literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_skill_loader.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_skill_loader.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..65f779f88ca4708a6cb35c30f9ce7f5c9b1f9bb1 GIT binary patch literal 8185 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 literal 0 HcmV?d00001 diff --git a/tests/test_agent_api.py b/tests/test_agent_api.py new file mode 100644 index 0000000..6dd7083 --- /dev/null +++ b/tests/test_agent_api.py @@ -0,0 +1,52 @@ +"""Starlette REST API integration tests using TestClient.""" + +import pytest +from starlette.testclient import TestClient +from app.agent import app + + +@pytest.fixture +def client(): + return TestClient(app) + + +def test_health_endpoint(client): + """GET /health returns healthy status.""" + response = client.get("/health") + assert response.status_code == 200 + data = response.json() + assert data["status"] == "healthy" + assert "gcp_solution_architecture_agent" in data["agent"] + + +def test_card_endpoint(client): + """GET /card returns agent capability card.""" + response = client.get("/card") + assert response.status_code == 200 + data = response.json() + assert data["name"] == "gcp_solution_architecture_agent" + assert "discover" in data["capabilities"]["phases"] + + +def test_generate_endpoint(client): + """POST /generate executes full agent workflow.""" + payload = { + "request": "Build an event-driven regional ingestion service", + "target_dir": ".", + } + response = client.post("/generate", json=payload) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + assert data["validation_passed"] is True + assert "artifacts" in data + assert "solution_guide" in data["artifacts"] + + +def test_validate_endpoint(client): + """POST /validate executes pre-deployment validation checks.""" + payload = {"target_dir": "."} + response = client.post("/validate", json=payload) + assert response.status_code == 200 + data = response.json() + assert data["valid"] is True diff --git a/tests/test_graph_workflow.py b/tests/test_graph_workflow.py new file mode 100644 index 0000000..e41a327 --- /dev/null +++ b/tests/test_graph_workflow.py @@ -0,0 +1,32 @@ +"""Integration tests for LangGraph StateGraph execution.""" + +import pytest +from app.workflows.gcp_architecture_graph import create_agent +from app.skills.loader import SkillLoader +from app.config import get_settings + + +def test_agent_graph_execution(): + """Verify execution of the 4-phase LangGraph StateGraph.""" + settings = get_settings() + loader = SkillLoader(settings.SKILLS_DIR) + loader.load_skills() + + agent = create_agent(loader) + initial_state = { + "workflow_request": "Event-driven regional HTTP application", + "target_dir": ".", + "active_skills": [], + } + + final_state = agent.invoke(initial_state) + + assert final_state.get("current_phase") == "package" + assert final_state.get("requirements_doc") is not None + assert final_state.get("architecture_doc") is not None + assert final_state.get("mermaid_diagram") is not None + assert final_state.get("terraform_code") is not None + assert final_state.get("validation_results") is not None + assert final_state.get("solution_guide") is not None + assert final_state.get("validation_passed") is True + assert len(final_state.get("active_skills", [])) == 4 diff --git a/tests/test_skill_loader.py b/tests/test_skill_loader.py new file mode 100644 index 0000000..4ca7298 --- /dev/null +++ b/tests/test_skill_loader.py @@ -0,0 +1,45 @@ +"""Unit tests for Local Dynamic Skill Loader.""" + +import pytest +from pathlib import Path +from app.skills.loader import SkillLoader, LocalSkill +from app.config import get_settings + + +def test_skill_loader_discovery(): + """Verify SkillLoader discovers all 4 phase skills under app/skills/.""" + settings = get_settings() + loader = SkillLoader(settings.SKILLS_DIR) + skills = loader.load_skills() + + assert len(skills) >= 4 + assert "requirements_discovery" in skills + assert "architecture_design" in skills + assert "validation_rules" in skills + assert "packaging_guide" in skills + + +def test_skill_loader_by_phase(): + """Verify filtering skills by phase.""" + settings = get_settings() + loader = SkillLoader(settings.SKILLS_DIR) + loader.load_skills() + + discover_skills = loader.get_skills_by_phase("discover") + assert len(discover_skills) == 1 + assert discover_skills[0].name == "requirements_discovery" + + design_skills = loader.get_skills_by_phase("design") + assert len(design_skills) == 1 + assert design_skills[0].name == "architecture_design" + + +def test_format_skills_for_prompt(): + """Verify formatting skill content into prompt text.""" + settings = get_settings() + loader = SkillLoader(settings.SKILLS_DIR) + loader.load_skills() + + prompt = loader.format_skills_for_prompt("discover") + assert "## Injected Skill Instructions" in prompt + assert "requirements_discovery" in prompt