cloudwright-ai-web 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ {
2
+ "priority": "",
3
+ "working": [],
4
+ "manual": []
5
+ }
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .env
7
+ *.db
8
+ !packages/core/cloudwright/data/catalog.db
9
+ node_modules/
10
+ .vite/
11
+ .ruff_cache/
12
+ .pytest_cache/
13
+ .mypy_cache/
14
+
15
+ # Codebase intelligence (auto-generated)
16
+ .planning/
@@ -0,0 +1,19 @@
1
+ # cloudwright-web
2
+
3
+ Web UI for Cloudwright. FastAPI backend wrapping core package + React frontend.
4
+
5
+ ## Backend
6
+
7
+ `backend/app.py` — FastAPI app with endpoints for design, cost, validate, export, catalog.
8
+
9
+ ## Frontend
10
+
11
+ React + TypeScript + Vite. Interactive architecture diagrams with React Flow,
12
+ cost tables, comparison views. Same chat experience as the CLI but visual.
13
+
14
+ ## Running
15
+
16
+ ```bash
17
+ cloudwright serve # starts both backend and frontend
18
+ cloudwright chat --web # same thing
19
+ ```
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: cloudwright-ai-web
3
+ Version: 0.1.0
4
+ Summary: Web UI for Cloudwright architecture intelligence
5
+ Project-URL: Homepage, https://github.com/xmpuspus/cloudwright
6
+ Project-URL: Repository, https://github.com/xmpuspus/cloudwright
7
+ Author: Xavier Puspus
8
+ License-Expression: MIT
9
+ Keywords: architecture,cloud,fastapi,web
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Requires-Python: >=3.12
14
+ Requires-Dist: cloudwright-ai<1,>=0.1.0
15
+ Requires-Dist: fastapi<1,>=0.116
16
+ Requires-Dist: uvicorn[standard]<1,>=0.35
17
+ Description-Content-Type: text/markdown
18
+
19
+ # cloudwright-web
20
+
21
+ Web UI for [Cloudwright](https://github.com/xmpuspus/cloudwright) architecture intelligence. FastAPI backend with React frontend.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install cloudwright[web]
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ```bash
32
+ cloudwright serve # starts the web server
33
+ cloudwright chat --web # same thing
34
+ ```
35
+
36
+ The API runs at `http://localhost:8000` with endpoints for design, cost, validate, export, and catalog operations.
37
+
38
+ See the [main project README](https://github.com/xmpuspus/cloudwright) for full documentation.
@@ -0,0 +1,20 @@
1
+ # cloudwright-web
2
+
3
+ Web UI for [Cloudwright](https://github.com/xmpuspus/cloudwright) architecture intelligence. FastAPI backend with React frontend.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install cloudwright[web]
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ cloudwright serve # starts the web server
15
+ cloudwright chat --web # same thing
16
+ ```
17
+
18
+ The API runs at `http://localhost:8000` with endpoints for design, cost, validate, export, and catalog operations.
19
+
20
+ See the [main project README](https://github.com/xmpuspus/cloudwright) for full documentation.
@@ -0,0 +1,11 @@
1
+ """Cloudwright Web — FastAPI backend for architecture intelligence."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+
6
+ def __getattr__(name: str):
7
+ if name == "app":
8
+ from cloudwright_web.app import app
9
+
10
+ return app
11
+ raise AttributeError(f"module 'cloudwright_web' has no attribute {name!r}")
@@ -0,0 +1,299 @@
1
+ """FastAPI backend wrapping the Cloudwright core package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Literal
8
+
9
+ from cloudwright import ArchSpec, Constraints
10
+ from cloudwright.architect import Architect
11
+ from cloudwright.catalog import Catalog
12
+ from cloudwright.cost import CostEngine
13
+ from cloudwright.differ import Differ
14
+ from cloudwright.exporter import FORMATS, export_spec
15
+ from cloudwright.validator import Validator
16
+ from fastapi import FastAPI, HTTPException
17
+ from fastapi.middleware.cors import CORSMiddleware
18
+ from fastapi.responses import FileResponse
19
+ from fastapi.staticfiles import StaticFiles
20
+ from pydantic import BaseModel, Field
21
+
22
+ log = logging.getLogger(__name__)
23
+
24
+ app = FastAPI(title="Cloudwright", version="0.1.0", description="Architecture intelligence for cloud engineers")
25
+
26
+ app.add_middleware(
27
+ CORSMiddleware,
28
+ allow_origins=["http://localhost:5173", "http://localhost:3000"],
29
+ allow_methods=["*"],
30
+ allow_headers=["*"],
31
+ )
32
+
33
+ # Lazy singletons
34
+ _architect: Architect | None = None
35
+ _catalog: Catalog | None = None
36
+ _cost_engine: CostEngine | None = None
37
+
38
+
39
+ def get_architect() -> Architect:
40
+ global _architect
41
+ if _architect is None:
42
+ _architect = Architect()
43
+ return _architect
44
+
45
+
46
+ def get_catalog() -> Catalog:
47
+ global _catalog
48
+ if _catalog is None:
49
+ _catalog = Catalog()
50
+ return _catalog
51
+
52
+
53
+ def get_cost_engine() -> CostEngine:
54
+ global _cost_engine
55
+ if _cost_engine is None:
56
+ _cost_engine = CostEngine()
57
+ return _cost_engine
58
+
59
+
60
+ # --- Request/Response models ---
61
+
62
+
63
+ class DesignRequest(BaseModel):
64
+ description: str = Field(..., min_length=5, max_length=2000)
65
+ provider: str = "aws"
66
+ region: str = "us-east-1"
67
+ budget_monthly: float | None = None
68
+ compliance: list[str] = Field(default_factory=list)
69
+
70
+
71
+ class ModifyRequest(BaseModel):
72
+ spec: dict
73
+ instruction: str = Field(..., min_length=3, max_length=2000)
74
+
75
+
76
+ class ValidateRequest(BaseModel):
77
+ spec: dict
78
+ compliance: list[str] = Field(default_factory=list)
79
+ well_architected: bool = False
80
+
81
+
82
+ class ExportRequest(BaseModel):
83
+ spec: dict
84
+ format: str
85
+
86
+
87
+ class DiffRequest(BaseModel):
88
+ old_spec: dict
89
+ new_spec: dict
90
+
91
+
92
+ class CostRequest(BaseModel):
93
+ spec: dict
94
+ compare_providers: list[str] = Field(default_factory=list)
95
+
96
+
97
+ class CatalogSearchRequest(BaseModel):
98
+ query: str | None = None
99
+ provider: str | None = None
100
+ vcpus: int | None = None
101
+ memory_gb: float | None = None
102
+ max_price_per_hour: float | None = None
103
+ limit: int = Field(default=20, ge=1, le=100)
104
+
105
+
106
+ class CatalogCompareRequest(BaseModel):
107
+ instance_names: list[str] = Field(..., min_length=2)
108
+
109
+
110
+ class ChatMessage(BaseModel):
111
+ role: Literal["user", "assistant"]
112
+ content: str
113
+
114
+
115
+ class ChatRequest(BaseModel):
116
+ message: str = Field(..., min_length=1, max_length=2000)
117
+ history: list[ChatMessage] = Field(default_factory=list)
118
+
119
+
120
+ # --- Endpoints ---
121
+
122
+
123
+ @app.get("/api/health")
124
+ def health():
125
+ try:
126
+ catalog = get_catalog()
127
+ # Quick check: can we search?
128
+ results = catalog.search(query="m5", limit=1)
129
+ return {"status": "ok", "catalog_loaded": True, "sample_count": len(results)}
130
+ except Exception:
131
+ return {"status": "ok", "catalog_loaded": False}
132
+
133
+
134
+ @app.post("/api/design")
135
+ def design(req: DesignRequest):
136
+ try:
137
+ architect = get_architect()
138
+ constraints = None
139
+ if req.budget_monthly or req.compliance:
140
+ constraints = Constraints(
141
+ budget_monthly=req.budget_monthly,
142
+ compliance=req.compliance,
143
+ )
144
+ spec = architect.design(req.description, constraints=constraints)
145
+ return {"spec": spec.model_dump(exclude_none=True), "yaml": spec.to_yaml()}
146
+ except Exception as e:
147
+ log.exception("Design endpoint failed")
148
+ raise HTTPException(status_code=500, detail="Internal server error") from e
149
+
150
+
151
+ @app.post("/api/modify")
152
+ def modify(req: ModifyRequest):
153
+ try:
154
+ architect = get_architect()
155
+ spec = ArchSpec.model_validate(req.spec)
156
+ updated = architect.modify(spec, req.instruction)
157
+ return {"spec": updated.model_dump(exclude_none=True), "yaml": updated.to_yaml()}
158
+ except Exception as e:
159
+ log.exception("Modify endpoint failed")
160
+ raise HTTPException(status_code=500, detail="Internal server error") from e
161
+
162
+
163
+ @app.post("/api/cost")
164
+ def cost(req: CostRequest):
165
+ try:
166
+ engine = get_cost_engine()
167
+ spec = ArchSpec.model_validate(req.spec)
168
+ estimate = engine.estimate(spec)
169
+
170
+ result = {"estimate": estimate.model_dump()}
171
+
172
+ if req.compare_providers:
173
+ architect = get_architect()
174
+ alternatives = architect.compare(spec, req.compare_providers)
175
+ result["alternatives"] = [a.model_dump(exclude_none=True) for a in alternatives]
176
+
177
+ return result
178
+ except Exception as e:
179
+ log.exception("Cost endpoint failed")
180
+ raise HTTPException(status_code=500, detail="Internal server error") from e
181
+
182
+
183
+ @app.post("/api/validate")
184
+ def validate(req: ValidateRequest):
185
+ try:
186
+ validator = Validator()
187
+ spec = ArchSpec.model_validate(req.spec)
188
+ frameworks = req.compliance if req.compliance else []
189
+ results = validator.validate(spec, compliance=frameworks or None, well_architected=req.well_architected)
190
+ return {"results": [r.model_dump() for r in results]}
191
+ except Exception as e:
192
+ log.exception("Validate endpoint failed")
193
+ raise HTTPException(status_code=500, detail="Internal server error") from e
194
+
195
+
196
+ @app.post("/api/export")
197
+ def export(req: ExportRequest):
198
+ try:
199
+ spec = ArchSpec.model_validate(req.spec)
200
+ if req.format not in FORMATS:
201
+ raise HTTPException(
202
+ status_code=400, detail=f"Unknown format: {req.format}. Supported: {', '.join(FORMATS)}"
203
+ )
204
+ content = export_spec(spec, req.format)
205
+ return {"content": content, "format": req.format}
206
+ except HTTPException:
207
+ raise
208
+ except Exception as e:
209
+ log.exception("Export endpoint failed")
210
+ raise HTTPException(status_code=500, detail="Internal server error") from e
211
+
212
+
213
+ @app.post("/api/diff")
214
+ def diff(req: DiffRequest):
215
+ try:
216
+ differ = Differ()
217
+ old = ArchSpec.model_validate(req.old_spec)
218
+ new = ArchSpec.model_validate(req.new_spec)
219
+ result = differ.diff(old, new)
220
+ return {"diff": result.model_dump()}
221
+ except Exception as e:
222
+ log.exception("Diff endpoint failed")
223
+ raise HTTPException(status_code=500, detail="Internal server error") from e
224
+
225
+
226
+ @app.post("/api/catalog/search")
227
+ def catalog_search(req: CatalogSearchRequest):
228
+ try:
229
+ catalog = get_catalog()
230
+ instances = catalog.search(
231
+ query=req.query,
232
+ vcpus=req.vcpus,
233
+ memory_gb=req.memory_gb,
234
+ provider=req.provider,
235
+ max_price_per_hour=req.max_price_per_hour,
236
+ limit=req.limit,
237
+ )
238
+ return {"instances": instances}
239
+ except Exception as e:
240
+ log.exception("Catalog search endpoint failed")
241
+ raise HTTPException(status_code=500, detail="Internal server error") from e
242
+
243
+
244
+ @app.post("/api/catalog/compare")
245
+ def catalog_compare(req: CatalogCompareRequest):
246
+ try:
247
+ catalog = get_catalog()
248
+ result = catalog.compare(*req.instance_names)
249
+ return {"comparison": result}
250
+ except Exception as e:
251
+ log.exception("Catalog compare endpoint failed")
252
+ raise HTTPException(status_code=500, detail="Internal server error") from e
253
+
254
+
255
+ @app.post("/api/chat")
256
+ def chat(req: ChatRequest):
257
+ try:
258
+ from cloudwright.architect import ConversationSession
259
+
260
+ architect = get_architect()
261
+ session = ConversationSession(llm=architect.llm)
262
+
263
+ # Replay history into the session
264
+ for msg in req.history:
265
+ session.history.append({"role": msg.role, "content": msg.content})
266
+
267
+ text, spec = session.send(req.message)
268
+ # Fallback: if session didn't extract a spec, try direct design
269
+ if spec is None and not req.history:
270
+ spec = architect.design(req.message)
271
+ text = f"Architecture: {spec.name}"
272
+ result: dict = {"reply": text, "history": session.history}
273
+ if spec:
274
+ result["spec"] = spec.model_dump(exclude_none=True)
275
+ result["yaml"] = spec.to_yaml()
276
+ return result
277
+ except Exception as e:
278
+ log.exception("Chat endpoint failed")
279
+ raise HTTPException(status_code=500, detail="Internal server error") from e
280
+
281
+
282
+ # Serve frontend static files if they exist
283
+ _frontend_dist = Path(__file__).parent.parent / "frontend" / "dist"
284
+ if _frontend_dist.exists():
285
+ app.mount("/assets", StaticFiles(directory=str(_frontend_dist / "assets")), name="assets")
286
+
287
+ @app.get("/{path:path}")
288
+ def serve_frontend(path: str):
289
+ file_path = (_frontend_dist / path).resolve()
290
+ if file_path.is_relative_to(_frontend_dist.resolve()) and file_path.is_file():
291
+ return FileResponse(str(file_path))
292
+ return FileResponse(str(_frontend_dist / "index.html"))
293
+
294
+
295
+ def serve(host: str = "127.0.0.1", port: int = 8000):
296
+ """Start the Cloudwright web server."""
297
+ import uvicorn
298
+
299
+ uvicorn.run(app, host=host, port=port)
File without changes
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Cloudwright — Architecture Intelligence</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
10
+ </style>
11
+ </head>
12
+ <body>
13
+ <div id="root"></div>
14
+ <script type="module" src="/src/main.tsx"></script>
15
+ </body>
16
+ </html>
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "cloudwright-web",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react": "^18.3.1",
13
+ "react-dom": "^18.3.1",
14
+ "@xyflow/react": "^12.3.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/react": "^18.3.0",
18
+ "@types/react-dom": "^18.3.0",
19
+ "@vitejs/plugin-react": "^4.3.0",
20
+ "typescript": "^5.5.0",
21
+ "vite": "^5.4.0"
22
+ }
23
+ }