mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-30 21:44:50 +02:00
[Haik]: removed app input functionality entirely as this will no longer be used
This commit is contained in:
@@ -9,11 +9,6 @@ class App(BaseModel):
|
||||
name: str
|
||||
description: str = ""
|
||||
icon: str = "view_quilt"
|
||||
input_schema: dict[str, Any] = Field(default_factory=lambda: {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
})
|
||||
files: dict[str, str] = Field(default_factory=dict)
|
||||
thumbnail: Optional[str] = None
|
||||
created_at: str = Field(default_factory=lambda: datetime.now().isoformat())
|
||||
@@ -24,11 +19,6 @@ class AppCreate(BaseModel):
|
||||
name: str
|
||||
description: str = ""
|
||||
icon: str = "view_quilt"
|
||||
input_schema: dict[str, Any] = Field(default_factory=lambda: {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
})
|
||||
files: dict[str, str] = Field(default_factory=dict)
|
||||
thumbnail: Optional[str] = None
|
||||
|
||||
@@ -37,21 +27,18 @@ class AppUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
input_schema: Optional[dict[str, Any]] = None
|
||||
files: Optional[dict[str, str]] = None
|
||||
thumbnail: Optional[str] = None
|
||||
|
||||
|
||||
class AppExecute(BaseModel):
|
||||
app_id: str
|
||||
input_data: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class AppExecuteResult(BaseModel):
|
||||
app_id: str
|
||||
app_name: str
|
||||
frontend_code: str
|
||||
input_data: dict[str, Any]
|
||||
backend_result: Optional[dict[str, Any]] = None
|
||||
stdout: Optional[str] = None
|
||||
stderr: Optional[str] = None
|
||||
|
||||
@@ -18,9 +18,7 @@ from backend.apps.app_builder.App import (
|
||||
from backend.config.paths import DB_ROOT
|
||||
from backend.apps.app_builder.executor import execute_backend_code
|
||||
from backend.apps.app_builder.templates import APP_BUILDER_SKILL, APP_BUILDER_TEMPLATE_FILES
|
||||
from backend.apps.app_builder.helpers import (
|
||||
validate_against_schema, inject_data_into_html, decode_data_param, walk_directory,
|
||||
)
|
||||
from backend.apps.app_builder.helpers import walk_directory
|
||||
|
||||
APP_BUILDER_DIR = os.path.join(DB_ROOT, "app_builder")
|
||||
APP_BUILDER_WORKSPACE_DIR = os.path.join(APP_BUILDER_DIR, "workspace")
|
||||
@@ -42,7 +40,7 @@ _store = PydanticStore[App](model_cls=App, data_dir=APP_BUILDER_DIR, not_found_d
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@app_builder.router.get("/workspace/{workspace_id}/serve/{filepath:path}")
|
||||
async def serve_workspace_file(workspace_id: str, filepath: str, _d: str = ""):
|
||||
async def serve_workspace_file(workspace_id: str, filepath: str):
|
||||
folder = os.path.join(APP_BUILDER_WORKSPACE_DIR, workspace_id)
|
||||
full_path = os.path.normpath(os.path.join(folder, filepath))
|
||||
if not full_path.startswith(os.path.normpath(folder)):
|
||||
@@ -51,22 +49,16 @@ async def serve_workspace_file(workspace_id: str, filepath: str, _d: str = ""):
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
with open(full_path) as f:
|
||||
content = f.read()
|
||||
if filepath == "index.html":
|
||||
input_json, result_json = decode_data_param(_d) if _d else ("{}", "null")
|
||||
content = inject_data_into_html(content, input_json, result_json)
|
||||
mime, _ = mimetypes.guess_type(filepath)
|
||||
return Response(content=content, media_type=mime or "text/plain")
|
||||
|
||||
|
||||
@app_builder.router.get("/{app_id}/serve/{filepath:path}")
|
||||
async def serve_app_file(app_id: str, filepath: str, _d: str = ""):
|
||||
async def serve_app_file(app_id: str, filepath: str):
|
||||
app = _store.load(app_id)
|
||||
content = app.files.get(filepath)
|
||||
if content is None:
|
||||
raise HTTPException(status_code=404, detail="File not found in app")
|
||||
if filepath == "index.html":
|
||||
input_json, result_json = decode_data_param(_d) if _d else ("{}", "null")
|
||||
content = inject_data_into_html(content, input_json, result_json)
|
||||
mime, _ = mimetypes.guess_type(filepath)
|
||||
return Response(content=content, media_type=mime or "text/plain")
|
||||
|
||||
@@ -168,8 +160,8 @@ async def create_app(body: AppCreate):
|
||||
now = datetime.now().isoformat()
|
||||
app = App(
|
||||
name=body.name, description=body.description, icon=body.icon,
|
||||
input_schema=body.input_schema, files=body.files,
|
||||
thumbnail=body.thumbnail, created_at=now, updated_at=now,
|
||||
files=body.files, thumbnail=body.thumbnail,
|
||||
created_at=now, updated_at=now,
|
||||
)
|
||||
_store.save(app)
|
||||
return {"ok": True, "app": app.model_dump()}
|
||||
@@ -199,14 +191,6 @@ async def delete_app(app_id: str):
|
||||
@app_builder.router.post("/execute")
|
||||
async def execute_app(body: AppExecute):
|
||||
app = _store.load(body.app_id)
|
||||
validation_err = validate_against_schema(body.input_data, app.input_schema)
|
||||
if validation_err:
|
||||
return AppExecuteResult(
|
||||
app_id=app.id, app_name=app.name,
|
||||
frontend_code=app.files.get("index.html", ""),
|
||||
input_data=body.input_data,
|
||||
backend_result=None, error=validation_err,
|
||||
).model_dump()
|
||||
|
||||
backend_code = app.files.get("backend.py")
|
||||
backend_result = None
|
||||
@@ -215,7 +199,7 @@ async def execute_app(body: AppExecute):
|
||||
error = None
|
||||
if backend_code:
|
||||
try:
|
||||
exec_result = await execute_backend_code(backend_code, body.input_data)
|
||||
exec_result = await execute_backend_code(backend_code)
|
||||
backend_result = exec_result.result
|
||||
stdout_text = exec_result.stdout
|
||||
stderr_text = exec_result.stderr
|
||||
@@ -225,7 +209,6 @@ async def execute_app(body: AppExecute):
|
||||
return AppExecuteResult(
|
||||
app_id=app.id, app_name=app.name,
|
||||
frontend_code=app.files.get("index.html", ""),
|
||||
input_data=body.input_data,
|
||||
backend_result=backend_result, stdout=stdout_text,
|
||||
stderr=stderr_text, error=error,
|
||||
).model_dump()
|
||||
|
||||
@@ -12,7 +12,6 @@ here is served directly to the live preview.
|
||||
|------|----------|---------|
|
||||
| `index.html` | **Yes** | Entry point. Must be a complete HTML document. This is the ONLY file the preview iframe loads — never rename it. |
|
||||
| `meta.json` | **Yes** | `{"name":"…","description":"…"}` — displayed in the UI header. Always write this. |
|
||||
| `schema.json` | Recommended | JSON Schema defining the input form (the "Test Input" tab). |
|
||||
| `backend.py` | Optional | Server-side Python executed before rendering. |
|
||||
| Everything else | Optional | JS, CSS, images, subdirectories — referenced from `index.html` via relative paths. |
|
||||
|
||||
@@ -20,77 +19,24 @@ here is served directly to the live preview.
|
||||
|
||||
- Name the main HTML file anything other than `index.html` — the platform
|
||||
will not find it and the preview will be blank.
|
||||
- Use `document.write()` — it breaks the injected data globals.
|
||||
- Assume any external server or API is available unless the user provides one.
|
||||
|
||||
---
|
||||
|
||||
## Injected globals
|
||||
|
||||
Before `index.html` loads, the platform injects two globals:
|
||||
|
||||
```javascript
|
||||
window.APP_BUILDER_INPUT // Object — structured input from the schema form
|
||||
window.APP_BUILDER_BACKEND_RESULT // Object | null — result from backend.py execution
|
||||
```
|
||||
|
||||
These are available immediately in any `<script>` tag. You can also listen for
|
||||
live updates when the user changes input:
|
||||
|
||||
```javascript
|
||||
window.addEventListener('app-builder-data-ready', () => {
|
||||
const input = window.APP_BUILDER_INPUT;
|
||||
const result = window.APP_BUILDER_BACKEND_RESULT;
|
||||
// re-render with new data
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## schema.json format
|
||||
|
||||
Standard JSON Schema. The platform renders a form from this automatically.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": { "type": "string", "default": "My Dashboard" },
|
||||
"count": { "type": "number", "default": 10 },
|
||||
"enabled": { "type": "boolean", "default": true },
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"default": ["alpha", "beta"]
|
||||
}
|
||||
},
|
||||
"required": ["title"]
|
||||
}
|
||||
```
|
||||
|
||||
Supported types: `string`, `number`, `integer`, `boolean`, `array`, `object`.
|
||||
Use `"default"` values so the preview works without manual input.
|
||||
|
||||
---
|
||||
|
||||
## backend.py
|
||||
|
||||
Optional server-side Python that runs before the frontend renders.
|
||||
It receives a global `input_data` dict (the schema form values) and must
|
||||
assign its result to a global `result` dict.
|
||||
It must assign its result to a global `result` dict.
|
||||
|
||||
```python
|
||||
# input_data is pre-populated from the schema form
|
||||
import json
|
||||
|
||||
result = {
|
||||
"processed_items": [item.upper() for item in input_data.get("items", [])],
|
||||
"items": ["alpha", "beta", "gamma"],
|
||||
"timestamp": "2024-01-01T00:00:00Z",
|
||||
}
|
||||
```
|
||||
|
||||
The `result` dict becomes `window.APP_BUILDER_BACKEND_RESULT` in the frontend.
|
||||
|
||||
---
|
||||
|
||||
## Multi-file projects
|
||||
@@ -102,7 +48,6 @@ workspace root, so relative imports work naturally:
|
||||
workspace/
|
||||
├── index.html
|
||||
├── meta.json
|
||||
├── schema.json
|
||||
├── styles/
|
||||
│ └── main.css
|
||||
├── components/
|
||||
@@ -146,9 +91,8 @@ import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
function App() {
|
||||
const input = window.APP_BUILDER_INPUT || {};
|
||||
return React.createElement('div', null,
|
||||
React.createElement('h1', null, input.title || 'Hello')
|
||||
React.createElement('h1', null, 'Hello')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -210,14 +154,9 @@ Other CDN libraries work too — use `https://esm.sh/` or `https://cdn.jsdelivr.
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1 id="title">Loading…</h1>
|
||||
<p id="desc"></p>
|
||||
<h1>Hello</h1>
|
||||
<p>Describe what you want to build and the agent will update this app.</p>
|
||||
</div>
|
||||
<script>
|
||||
const input = window.APP_BUILDER_INPUT || {};
|
||||
document.getElementById('title').textContent = input.title || 'Untitled';
|
||||
document.getElementById('desc').textContent = input.description || 'No description provided.';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
@@ -16,12 +16,12 @@ class BackendExecResult:
|
||||
stderr: str
|
||||
|
||||
|
||||
async def execute_backend_code(code: str, input_data: dict) -> BackendExecResult:
|
||||
async def execute_backend_code(code: str) -> BackendExecResult:
|
||||
"""Execute user-provided Python code in a subprocess.
|
||||
|
||||
The code receives ``input_data`` as a global dict and must assign its
|
||||
result to a global ``result`` dict. User print() calls are captured
|
||||
separately from the result via an in-process StringIO redirect.
|
||||
The code must assign its result to a global ``result`` dict.
|
||||
User print() calls are captured separately from the result via
|
||||
an in-process StringIO redirect.
|
||||
"""
|
||||
|
||||
preamble = (
|
||||
@@ -29,7 +29,6 @@ async def execute_backend_code(code: str, input_data: dict) -> BackendExecResult
|
||||
"_orig_stdout = sys.stdout\n"
|
||||
"_capture = io.StringIO()\n"
|
||||
"sys.stdout = _capture\n"
|
||||
"input_data = json.loads(sys.stdin.read())\n"
|
||||
"result = {}\n"
|
||||
)
|
||||
postamble = (
|
||||
@@ -40,14 +39,13 @@ async def execute_backend_code(code: str, input_data: dict) -> BackendExecResult
|
||||
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
sys.executable, "-c", wrapper,
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
|
||||
try:
|
||||
stdout, stderr = await asyncio.wait_for(
|
||||
proc.communicate(input=json.dumps(input_data).encode()),
|
||||
proc.communicate(),
|
||||
timeout=TIMEOUT_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
|
||||
@@ -1,60 +1,9 @@
|
||||
"""Pure helpers for data injection, validation, and directory walking."""
|
||||
"""Pure helpers for the app builder."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
|
||||
from jsonschema import validate as schema_validate, ValidationError as SchemaValidationError
|
||||
|
||||
|
||||
def validate_against_schema(data: dict, schema: dict) -> str | None:
|
||||
"""Validate *data* against *schema*. Return an error string or None."""
|
||||
try:
|
||||
schema_validate(instance=data, schema=schema)
|
||||
return None
|
||||
except SchemaValidationError as exc:
|
||||
path = " -> ".join(str(p) for p in exc.absolute_path) if exc.absolute_path else "(root)"
|
||||
return f"Schema validation failed at {path}: {exc.message}"
|
||||
|
||||
|
||||
def build_data_injection(input_json: str, result_json: str) -> str:
|
||||
return (
|
||||
"<script>\n"
|
||||
"(function() {\n"
|
||||
" window.APP_BUILDER_INPUT = " + input_json + ";\n"
|
||||
" window.APP_BUILDER_BACKEND_RESULT = " + result_json + ";\n"
|
||||
" window.addEventListener('message', function(e) {\n"
|
||||
" if (e.data && e.data.type === 'APP_BUILDER_DATA') {\n"
|
||||
" window.APP_BUILDER_INPUT = e.data.input || {};\n"
|
||||
" window.APP_BUILDER_BACKEND_RESULT = e.data.backendResult || null;\n"
|
||||
" window.dispatchEvent(new CustomEvent('app-builder-data-ready'));\n"
|
||||
" }\n"
|
||||
" });\n"
|
||||
"})();\n"
|
||||
"</script>"
|
||||
)
|
||||
|
||||
|
||||
def inject_data_into_html(html: str, input_json: str = "{}", result_json: str = "null") -> str:
|
||||
injection = build_data_injection(input_json, result_json)
|
||||
if "</head>" in html:
|
||||
return html.replace("</head>", f"{injection}\n</head>", 1)
|
||||
if "<body" in html:
|
||||
return html.replace("<body", f"{injection}\n<body", 1)
|
||||
return f"{injection}\n{html}"
|
||||
|
||||
|
||||
def decode_data_param(d: str) -> tuple[str, str]:
|
||||
try:
|
||||
decoded = json.loads(base64.b64decode(d))
|
||||
input_json = json.dumps(decoded.get("i", {}))
|
||||
result_json = json.dumps(decoded.get("r", None))
|
||||
return input_json, result_json
|
||||
except Exception:
|
||||
return "{}", "null"
|
||||
|
||||
|
||||
def walk_directory(folder: str) -> dict[str, str]:
|
||||
files: dict[str, str] = {}
|
||||
|
||||
@@ -45,21 +45,11 @@ APP_BUILDER_TEMPLATE_INDEX = """\
|
||||
<p id="desc">Describe what you want to build and the agent will update this app.</p>
|
||||
</div>
|
||||
<script>
|
||||
const input = window.APP_BUILDER_INPUT || {};
|
||||
const result = window.APP_BUILDER_BACKEND_RESULT || null;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
APP_BUILDER_TEMPLATE_SCHEMA = """\
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
"""
|
||||
|
||||
APP_BUILDER_TEMPLATE_META = """\
|
||||
{
|
||||
"name": "",
|
||||
@@ -69,6 +59,5 @@ APP_BUILDER_TEMPLATE_META = """\
|
||||
|
||||
APP_BUILDER_TEMPLATE_FILES = {
|
||||
"index.html": APP_BUILDER_TEMPLATE_INDEX,
|
||||
"schema.json": APP_BUILDER_TEMPLATE_SCHEMA,
|
||||
"meta.json": APP_BUILDER_TEMPLATE_META,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user