namel3ss 0.1.0a0__py3-none-any.whl
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.
- namel3ss/__init__.py +4 -0
- namel3ss/ast/__init__.py +5 -0
- namel3ss/ast/agents.py +13 -0
- namel3ss/ast/ai.py +23 -0
- namel3ss/ast/base.py +10 -0
- namel3ss/ast/expressions.py +55 -0
- namel3ss/ast/nodes.py +86 -0
- namel3ss/ast/pages.py +43 -0
- namel3ss/ast/program.py +22 -0
- namel3ss/ast/records.py +27 -0
- namel3ss/ast/statements.py +107 -0
- namel3ss/ast/tool.py +11 -0
- namel3ss/cli/__init__.py +2 -0
- namel3ss/cli/actions_mode.py +39 -0
- namel3ss/cli/app_loader.py +22 -0
- namel3ss/cli/commands/action.py +27 -0
- namel3ss/cli/commands/run.py +43 -0
- namel3ss/cli/commands/ui.py +26 -0
- namel3ss/cli/commands/validate.py +23 -0
- namel3ss/cli/format_mode.py +30 -0
- namel3ss/cli/io/json_io.py +19 -0
- namel3ss/cli/io/read_source.py +16 -0
- namel3ss/cli/json_io.py +21 -0
- namel3ss/cli/lint_mode.py +29 -0
- namel3ss/cli/main.py +135 -0
- namel3ss/cli/new_mode.py +146 -0
- namel3ss/cli/runner.py +28 -0
- namel3ss/cli/studio_mode.py +22 -0
- namel3ss/cli/ui_mode.py +14 -0
- namel3ss/config/__init__.py +4 -0
- namel3ss/config/dotenv.py +33 -0
- namel3ss/config/loader.py +83 -0
- namel3ss/config/model.py +49 -0
- namel3ss/errors/__init__.py +2 -0
- namel3ss/errors/base.py +34 -0
- namel3ss/errors/render.py +22 -0
- namel3ss/format/__init__.py +3 -0
- namel3ss/format/formatter.py +18 -0
- namel3ss/format/rules.py +97 -0
- namel3ss/ir/__init__.py +3 -0
- namel3ss/ir/lowering/__init__.py +4 -0
- namel3ss/ir/lowering/agents.py +42 -0
- namel3ss/ir/lowering/ai.py +45 -0
- namel3ss/ir/lowering/expressions.py +49 -0
- namel3ss/ir/lowering/flow.py +21 -0
- namel3ss/ir/lowering/pages.py +48 -0
- namel3ss/ir/lowering/program.py +34 -0
- namel3ss/ir/lowering/records.py +25 -0
- namel3ss/ir/lowering/statements.py +122 -0
- namel3ss/ir/lowering/tools.py +16 -0
- namel3ss/ir/model/__init__.py +50 -0
- namel3ss/ir/model/agents.py +33 -0
- namel3ss/ir/model/ai.py +31 -0
- namel3ss/ir/model/base.py +20 -0
- namel3ss/ir/model/expressions.py +50 -0
- namel3ss/ir/model/pages.py +43 -0
- namel3ss/ir/model/program.py +28 -0
- namel3ss/ir/model/statements.py +76 -0
- namel3ss/ir/model/tools.py +11 -0
- namel3ss/ir/nodes.py +88 -0
- namel3ss/lexer/__init__.py +2 -0
- namel3ss/lexer/lexer.py +152 -0
- namel3ss/lexer/tokens.py +98 -0
- namel3ss/lint/__init__.py +4 -0
- namel3ss/lint/engine.py +125 -0
- namel3ss/lint/semantic.py +45 -0
- namel3ss/lint/text_scan.py +70 -0
- namel3ss/lint/types.py +22 -0
- namel3ss/parser/__init__.py +3 -0
- namel3ss/parser/agent.py +78 -0
- namel3ss/parser/ai.py +113 -0
- namel3ss/parser/constraints.py +37 -0
- namel3ss/parser/core.py +166 -0
- namel3ss/parser/expressions.py +105 -0
- namel3ss/parser/flow.py +37 -0
- namel3ss/parser/pages.py +76 -0
- namel3ss/parser/program.py +45 -0
- namel3ss/parser/records.py +66 -0
- namel3ss/parser/statements/__init__.py +27 -0
- namel3ss/parser/statements/control_flow.py +116 -0
- namel3ss/parser/statements/core.py +66 -0
- namel3ss/parser/statements/data.py +17 -0
- namel3ss/parser/statements/letset.py +22 -0
- namel3ss/parser/statements.py +1 -0
- namel3ss/parser/tokens.py +35 -0
- namel3ss/parser/tool.py +29 -0
- namel3ss/runtime/__init__.py +3 -0
- namel3ss/runtime/ai/http/client.py +24 -0
- namel3ss/runtime/ai/mock_provider.py +5 -0
- namel3ss/runtime/ai/provider.py +29 -0
- namel3ss/runtime/ai/providers/__init__.py +18 -0
- namel3ss/runtime/ai/providers/_shared/errors.py +20 -0
- namel3ss/runtime/ai/providers/_shared/parse.py +18 -0
- namel3ss/runtime/ai/providers/anthropic.py +55 -0
- namel3ss/runtime/ai/providers/gemini.py +50 -0
- namel3ss/runtime/ai/providers/mistral.py +51 -0
- namel3ss/runtime/ai/providers/mock.py +23 -0
- namel3ss/runtime/ai/providers/ollama.py +39 -0
- namel3ss/runtime/ai/providers/openai.py +55 -0
- namel3ss/runtime/ai/providers/registry.py +38 -0
- namel3ss/runtime/ai/trace.py +18 -0
- namel3ss/runtime/executor/__init__.py +3 -0
- namel3ss/runtime/executor/agents.py +91 -0
- namel3ss/runtime/executor/ai_runner.py +90 -0
- namel3ss/runtime/executor/api.py +54 -0
- namel3ss/runtime/executor/assign.py +40 -0
- namel3ss/runtime/executor/context.py +31 -0
- namel3ss/runtime/executor/executor.py +77 -0
- namel3ss/runtime/executor/expr_eval.py +110 -0
- namel3ss/runtime/executor/records_ops.py +64 -0
- namel3ss/runtime/executor/result.py +13 -0
- namel3ss/runtime/executor/signals.py +6 -0
- namel3ss/runtime/executor/statements.py +99 -0
- namel3ss/runtime/memory/manager.py +52 -0
- namel3ss/runtime/memory/profile.py +17 -0
- namel3ss/runtime/memory/semantic.py +20 -0
- namel3ss/runtime/memory/short_term.py +18 -0
- namel3ss/runtime/records/service.py +105 -0
- namel3ss/runtime/store/__init__.py +2 -0
- namel3ss/runtime/store/memory_store.py +62 -0
- namel3ss/runtime/tools/registry.py +13 -0
- namel3ss/runtime/ui/__init__.py +2 -0
- namel3ss/runtime/ui/actions.py +124 -0
- namel3ss/runtime/validators/__init__.py +2 -0
- namel3ss/runtime/validators/constraints.py +126 -0
- namel3ss/schema/__init__.py +2 -0
- namel3ss/schema/records.py +52 -0
- namel3ss/studio/__init__.py +4 -0
- namel3ss/studio/api.py +115 -0
- namel3ss/studio/edit/__init__.py +3 -0
- namel3ss/studio/edit/ops.py +80 -0
- namel3ss/studio/edit/selectors.py +74 -0
- namel3ss/studio/edit/transform.py +39 -0
- namel3ss/studio/server.py +175 -0
- namel3ss/studio/session.py +11 -0
- namel3ss/studio/web/app.js +248 -0
- namel3ss/studio/web/index.html +44 -0
- namel3ss/studio/web/styles.css +42 -0
- namel3ss/templates/__init__.py +3 -0
- namel3ss/templates/__pycache__/__init__.cpython-312.pyc +0 -0
- namel3ss/templates/ai_assistant/.gitignore +1 -0
- namel3ss/templates/ai_assistant/README.md +10 -0
- namel3ss/templates/ai_assistant/app.ai +30 -0
- namel3ss/templates/crud/.gitignore +1 -0
- namel3ss/templates/crud/README.md +10 -0
- namel3ss/templates/crud/app.ai +26 -0
- namel3ss/templates/multi_agent/.gitignore +1 -0
- namel3ss/templates/multi_agent/README.md +10 -0
- namel3ss/templates/multi_agent/app.ai +43 -0
- namel3ss/ui/__init__.py +2 -0
- namel3ss/ui/manifest.py +220 -0
- namel3ss/utils/__init__.py +2 -0
- namel3ss-0.1.0a0.dist-info/METADATA +123 -0
- namel3ss-0.1.0a0.dist-info/RECORD +157 -0
- namel3ss-0.1.0a0.dist-info/WHEEL +5 -0
- namel3ss-0.1.0a0.dist-info/entry_points.txt +2 -0
- namel3ss-0.1.0a0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from namel3ss.errors.base import Namel3ssError
|
|
10
|
+
from namel3ss.errors.render import format_error
|
|
11
|
+
from namel3ss.studio.api import (
|
|
12
|
+
apply_edit,
|
|
13
|
+
execute_action,
|
|
14
|
+
get_actions_payload,
|
|
15
|
+
get_lint_payload,
|
|
16
|
+
get_summary_payload,
|
|
17
|
+
get_ui_payload,
|
|
18
|
+
)
|
|
19
|
+
from namel3ss.studio.session import SessionState
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class StudioRequestHandler(SimpleHTTPRequestHandler):
|
|
23
|
+
def log_message(self, format: str, *args: Any) -> None: # pragma: no cover - silence
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
def _read_source(self) -> str:
|
|
27
|
+
path = Path(self.server.app_path) # type: ignore[attr-defined]
|
|
28
|
+
return path.read_text(encoding="utf-8")
|
|
29
|
+
|
|
30
|
+
def _get_session(self) -> SessionState:
|
|
31
|
+
return self.server.session_state # type: ignore[attr-defined]
|
|
32
|
+
|
|
33
|
+
def _respond_json(self, payload: dict, status: int = 200) -> None:
|
|
34
|
+
data = json.dumps(payload).encode("utf-8")
|
|
35
|
+
self.send_response(status)
|
|
36
|
+
self.send_header("Content-Type", "application/json")
|
|
37
|
+
self.send_header("Content-Length", str(len(data)))
|
|
38
|
+
self.end_headers()
|
|
39
|
+
self.wfile.write(data)
|
|
40
|
+
|
|
41
|
+
def do_GET(self) -> None: # noqa: N802
|
|
42
|
+
if self.path.startswith("/api/"):
|
|
43
|
+
self.handle_api()
|
|
44
|
+
return
|
|
45
|
+
self.handle_static()
|
|
46
|
+
|
|
47
|
+
def do_POST(self) -> None: # noqa: N802
|
|
48
|
+
if self.path.startswith("/api/"):
|
|
49
|
+
self.handle_api_post()
|
|
50
|
+
return
|
|
51
|
+
self.send_error(404)
|
|
52
|
+
|
|
53
|
+
def handle_static(self) -> None:
|
|
54
|
+
web_root = Path(__file__).parent / "web"
|
|
55
|
+
if self.path in {"/", "/index.html"}:
|
|
56
|
+
file_path = web_root / "index.html"
|
|
57
|
+
else:
|
|
58
|
+
file_path = web_root / self.path.lstrip("/")
|
|
59
|
+
if not file_path.exists():
|
|
60
|
+
self.send_error(404)
|
|
61
|
+
return
|
|
62
|
+
content = file_path.read_bytes()
|
|
63
|
+
content_type = "text/html"
|
|
64
|
+
if file_path.suffix == ".js":
|
|
65
|
+
content_type = "application/javascript"
|
|
66
|
+
if file_path.suffix == ".css":
|
|
67
|
+
content_type = "text/css"
|
|
68
|
+
self.send_response(200)
|
|
69
|
+
self.send_header("Content-Type", content_type)
|
|
70
|
+
self.send_header("Content-Length", str(len(content)))
|
|
71
|
+
self.end_headers()
|
|
72
|
+
self.wfile.write(content)
|
|
73
|
+
|
|
74
|
+
def handle_api(self) -> None:
|
|
75
|
+
try:
|
|
76
|
+
source = self._read_source()
|
|
77
|
+
except Exception as err: # pragma: no cover - IO error edge
|
|
78
|
+
payload = {"ok": False, "error": f"Cannot read source: {err}"}
|
|
79
|
+
self._respond_json(payload, status=500)
|
|
80
|
+
return
|
|
81
|
+
if self.path == "/api/summary":
|
|
82
|
+
payload = get_summary_payload(source, self.server.app_path) # type: ignore[attr-defined]
|
|
83
|
+
status = 200 if payload.get("ok") else 400
|
|
84
|
+
self._respond_json(payload, status=status)
|
|
85
|
+
return
|
|
86
|
+
if self.path == "/api/ui":
|
|
87
|
+
payload = get_ui_payload(source, self._get_session())
|
|
88
|
+
status = 200 if payload.get("ok", True) else 400
|
|
89
|
+
self._respond_json(payload, status=status)
|
|
90
|
+
return
|
|
91
|
+
if self.path == "/api/actions":
|
|
92
|
+
payload = get_actions_payload(source)
|
|
93
|
+
status = 200 if payload.get("ok") else 400
|
|
94
|
+
self._respond_json(payload, status=status)
|
|
95
|
+
return
|
|
96
|
+
if self.path == "/api/lint":
|
|
97
|
+
payload = get_lint_payload(source)
|
|
98
|
+
self._respond_json(payload, status=200)
|
|
99
|
+
return
|
|
100
|
+
self.send_error(404)
|
|
101
|
+
|
|
102
|
+
def handle_api_post(self) -> None:
|
|
103
|
+
length = int(self.headers.get("Content-Length", "0"))
|
|
104
|
+
raw_body = self.rfile.read(length) if length else b""
|
|
105
|
+
try:
|
|
106
|
+
body = json.loads(raw_body.decode("utf-8") or "{}")
|
|
107
|
+
except json.JSONDecodeError:
|
|
108
|
+
self._respond_json({"ok": False, "error": "Invalid JSON body"}, status=400)
|
|
109
|
+
return
|
|
110
|
+
try:
|
|
111
|
+
source = self._read_source()
|
|
112
|
+
except Exception as err: # pragma: no cover
|
|
113
|
+
payload = {"ok": False, "error": f"Cannot read source: {err}"}
|
|
114
|
+
self._respond_json(payload, status=500)
|
|
115
|
+
return
|
|
116
|
+
if self.path == "/api/edit":
|
|
117
|
+
if not isinstance(body, dict):
|
|
118
|
+
self._respond_json({"ok": False, "error": "Body must be a JSON object"}, status=400)
|
|
119
|
+
return
|
|
120
|
+
op = body.get("op")
|
|
121
|
+
target = body.get("target")
|
|
122
|
+
value = body.get("value", "")
|
|
123
|
+
if not isinstance(op, str):
|
|
124
|
+
self._respond_json({"ok": False, "error": "Edit op is required"}, status=400)
|
|
125
|
+
return
|
|
126
|
+
if not isinstance(target, dict):
|
|
127
|
+
self._respond_json({"ok": False, "error": "Edit target is required"}, status=400)
|
|
128
|
+
return
|
|
129
|
+
if not isinstance(value, str):
|
|
130
|
+
self._respond_json({"ok": False, "error": "Edit value must be a string"}, status=400)
|
|
131
|
+
return
|
|
132
|
+
try:
|
|
133
|
+
resp = apply_edit(self.server.app_path, op, target, value, self._get_session()) # type: ignore[attr-defined]
|
|
134
|
+
self._respond_json(resp, status=200)
|
|
135
|
+
return
|
|
136
|
+
except Namel3ssError as err:
|
|
137
|
+
self._respond_json({"ok": False, "error": format_error(err, source)}, status=400)
|
|
138
|
+
return
|
|
139
|
+
if self.path == "/api/action":
|
|
140
|
+
if not isinstance(body, dict):
|
|
141
|
+
self._respond_json({"ok": False, "error": "Body must be a JSON object"}, status=400)
|
|
142
|
+
return
|
|
143
|
+
action_id = body.get("id")
|
|
144
|
+
payload = body.get("payload") or {}
|
|
145
|
+
if not isinstance(action_id, str):
|
|
146
|
+
self._respond_json({"ok": False, "error": "Action id is required"}, status=400)
|
|
147
|
+
return
|
|
148
|
+
if not isinstance(payload, dict):
|
|
149
|
+
self._respond_json({"ok": False, "error": "Payload must be an object"}, status=400)
|
|
150
|
+
return
|
|
151
|
+
try:
|
|
152
|
+
resp = execute_action(source, self._get_session(), action_id, payload)
|
|
153
|
+
status = 200 if resp.get("ok", True) else 200
|
|
154
|
+
self._respond_json(resp, status=status)
|
|
155
|
+
return
|
|
156
|
+
except Namel3ssError as err:
|
|
157
|
+
self._respond_json({"ok": False, "error": format_error(err, source)}, status=400)
|
|
158
|
+
return
|
|
159
|
+
if self.path == "/api/reset":
|
|
160
|
+
self.server.session_state = SessionState() # type: ignore[attr-defined]
|
|
161
|
+
self._respond_json({"ok": True}, status=200)
|
|
162
|
+
return
|
|
163
|
+
self.send_error(404)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def start_server(app_path: str, port: int) -> None:
|
|
167
|
+
handler = StudioRequestHandler
|
|
168
|
+
server = HTTPServer(("127.0.0.1", port), handler)
|
|
169
|
+
server.app_path = app_path # type: ignore[attr-defined]
|
|
170
|
+
server.session_state = SessionState() # type: ignore[attr-defined]
|
|
171
|
+
print(f"Studio: http://127.0.0.1:{port}/")
|
|
172
|
+
try:
|
|
173
|
+
server.serve_forever()
|
|
174
|
+
finally:
|
|
175
|
+
server.server_close()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from namel3ss.runtime.store.memory_store import MemoryStore
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class SessionState:
|
|
10
|
+
state: dict = field(default_factory=dict)
|
|
11
|
+
store: MemoryStore = field(default_factory=MemoryStore)
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
async function fetchJson(path, options) {
|
|
2
|
+
const res = await fetch(path, options);
|
|
3
|
+
return res.json();
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function renderSummary(data) {
|
|
7
|
+
const el = document.getElementById("summary");
|
|
8
|
+
if (!data.ok) {
|
|
9
|
+
el.textContent = data.error || "Error";
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
el.textContent = JSON.stringify(data.counts, null, 2);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function renderActions(data) {
|
|
16
|
+
const el = document.getElementById("actions");
|
|
17
|
+
if (!data.ok) {
|
|
18
|
+
el.textContent = data.error || "Error";
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
el.textContent = data.actions
|
|
22
|
+
.map((a) => {
|
|
23
|
+
const parts = [a.id, a.type];
|
|
24
|
+
if (a.flow) parts.push(`flow=${a.flow}`);
|
|
25
|
+
if (a.record) parts.push(`record=${a.record}`);
|
|
26
|
+
return parts.join(" ");
|
|
27
|
+
})
|
|
28
|
+
.join("\n");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renderLint(data) {
|
|
32
|
+
const el = document.getElementById("lint");
|
|
33
|
+
if (!data.ok && data.error) {
|
|
34
|
+
el.textContent = data.error;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
el.textContent =
|
|
38
|
+
data.findings
|
|
39
|
+
.map((f) => `${f.severity} ${f.code} ${f.message} (${f.line}:${f.column})`)
|
|
40
|
+
.join("\n") || "OK";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function renderState(data) {
|
|
44
|
+
const el = document.getElementById("state");
|
|
45
|
+
el.textContent = data ? JSON.stringify(data, null, 2) : "{}";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function renderTraces(data) {
|
|
49
|
+
const el = document.getElementById("traces");
|
|
50
|
+
el.textContent = data ? JSON.stringify(data, null, 2) : "[]";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function executeAction(actionId, payload) {
|
|
54
|
+
const res = await fetch("/api/action", {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: { "Content-Type": "application/json" },
|
|
57
|
+
body: JSON.stringify({ id: actionId, payload }),
|
|
58
|
+
});
|
|
59
|
+
const data = await res.json();
|
|
60
|
+
if (!data.ok && data.error) {
|
|
61
|
+
alert(data.error);
|
|
62
|
+
}
|
|
63
|
+
if (!data.ok && data.errors) {
|
|
64
|
+
return data;
|
|
65
|
+
}
|
|
66
|
+
if (data.state) {
|
|
67
|
+
renderState(data.state);
|
|
68
|
+
}
|
|
69
|
+
if (data.traces) {
|
|
70
|
+
renderTraces(data.traces);
|
|
71
|
+
}
|
|
72
|
+
if (data.ui) {
|
|
73
|
+
renderUI(data.ui);
|
|
74
|
+
}
|
|
75
|
+
return data;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function performEdit(op, elementId, pageName, value) {
|
|
79
|
+
const res = await fetch("/api/edit", {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: { "Content-Type": "application/json" },
|
|
82
|
+
body: JSON.stringify({ op, target: { element_id: elementId, page: pageName }, value }),
|
|
83
|
+
});
|
|
84
|
+
const data = await res.json();
|
|
85
|
+
if (!data.ok) {
|
|
86
|
+
alert(data.error || "Edit failed");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
renderSummary(data.summary);
|
|
90
|
+
renderActions(data.actions);
|
|
91
|
+
renderLint(data.lint);
|
|
92
|
+
if (data.ui) {
|
|
93
|
+
renderUI(data.ui);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function renderUI(manifest) {
|
|
98
|
+
const select = document.getElementById("pageSelect");
|
|
99
|
+
const uiContainer = document.getElementById("ui");
|
|
100
|
+
const pages = manifest.pages || [];
|
|
101
|
+
const currentSelection = select.value;
|
|
102
|
+
select.innerHTML = "";
|
|
103
|
+
pages.forEach((p, idx) => {
|
|
104
|
+
const opt = document.createElement("option");
|
|
105
|
+
opt.value = p.name;
|
|
106
|
+
opt.textContent = p.name;
|
|
107
|
+
if (p.name === currentSelection || (currentSelection === "" && idx === 0)) {
|
|
108
|
+
opt.selected = true;
|
|
109
|
+
}
|
|
110
|
+
select.appendChild(opt);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
function renderPage(pageName) {
|
|
114
|
+
uiContainer.innerHTML = "";
|
|
115
|
+
const page = pages.find((p) => p.name === pageName) || pages[0];
|
|
116
|
+
if (!page) return;
|
|
117
|
+
page.elements.forEach((el) => {
|
|
118
|
+
const div = document.createElement("div");
|
|
119
|
+
div.className = "element";
|
|
120
|
+
if (el.type === "title") {
|
|
121
|
+
const h = document.createElement("h3");
|
|
122
|
+
h.textContent = el.value;
|
|
123
|
+
div.appendChild(h);
|
|
124
|
+
const btn = document.createElement("button");
|
|
125
|
+
btn.textContent = "Edit";
|
|
126
|
+
btn.onclick = () => showEditField(el, page.name, "set_title");
|
|
127
|
+
div.appendChild(btn);
|
|
128
|
+
} else if (el.type === "text") {
|
|
129
|
+
const p = document.createElement("p");
|
|
130
|
+
p.textContent = el.value;
|
|
131
|
+
div.appendChild(p);
|
|
132
|
+
const btn = document.createElement("button");
|
|
133
|
+
btn.textContent = "Edit";
|
|
134
|
+
btn.onclick = () => showEditField(el, page.name, "set_text");
|
|
135
|
+
div.appendChild(btn);
|
|
136
|
+
} else if (el.type === "button") {
|
|
137
|
+
const btn = document.createElement("button");
|
|
138
|
+
btn.textContent = el.label;
|
|
139
|
+
btn.onclick = () => executeAction(el.action_id, {});
|
|
140
|
+
div.appendChild(btn);
|
|
141
|
+
const rename = document.createElement("button");
|
|
142
|
+
rename.textContent = "Rename";
|
|
143
|
+
rename.onclick = () => showEditField(el, page.name, "set_button_label");
|
|
144
|
+
div.appendChild(rename);
|
|
145
|
+
} else if (el.type === "form") {
|
|
146
|
+
const form = document.createElement("form");
|
|
147
|
+
form.innerHTML = `<strong>Form: ${el.record}</strong>`;
|
|
148
|
+
(el.fields || []).forEach((f) => {
|
|
149
|
+
const label = document.createElement("label");
|
|
150
|
+
label.textContent = f.name;
|
|
151
|
+
const input = document.createElement("input");
|
|
152
|
+
input.name = f.name;
|
|
153
|
+
label.appendChild(input);
|
|
154
|
+
form.appendChild(label);
|
|
155
|
+
});
|
|
156
|
+
const submit = document.createElement("button");
|
|
157
|
+
submit.type = "submit";
|
|
158
|
+
submit.textContent = "Submit";
|
|
159
|
+
form.appendChild(submit);
|
|
160
|
+
const errors = document.createElement("div");
|
|
161
|
+
errors.className = "errors";
|
|
162
|
+
form.appendChild(errors);
|
|
163
|
+
form.onsubmit = async (e) => {
|
|
164
|
+
e.preventDefault();
|
|
165
|
+
const values = {};
|
|
166
|
+
(el.fields || []).forEach((f) => {
|
|
167
|
+
const input = form.querySelector(`input[name="${f.name}"]`);
|
|
168
|
+
values[f.name] = input ? input.value : "";
|
|
169
|
+
});
|
|
170
|
+
const result = await executeAction(el.action_id, { values });
|
|
171
|
+
if (!result.ok && result.errors) {
|
|
172
|
+
errors.textContent = result.errors.map((err) => `${err.field}: ${err.message}`).join("; ");
|
|
173
|
+
} else if (!result.ok && result.error) {
|
|
174
|
+
errors.textContent = result.error;
|
|
175
|
+
} else {
|
|
176
|
+
errors.textContent = "";
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
div.appendChild(form);
|
|
180
|
+
} else if (el.type === "table") {
|
|
181
|
+
const table = document.createElement("table");
|
|
182
|
+
const header = document.createElement("tr");
|
|
183
|
+
(el.columns || []).forEach((c) => {
|
|
184
|
+
const th = document.createElement("th");
|
|
185
|
+
th.textContent = c.name;
|
|
186
|
+
header.appendChild(th);
|
|
187
|
+
});
|
|
188
|
+
table.appendChild(header);
|
|
189
|
+
(el.rows || []).forEach((row) => {
|
|
190
|
+
const tr = document.createElement("tr");
|
|
191
|
+
(el.columns || []).forEach((c) => {
|
|
192
|
+
const td = document.createElement("td");
|
|
193
|
+
td.textContent = row[c.name] ?? "";
|
|
194
|
+
tr.appendChild(td);
|
|
195
|
+
});
|
|
196
|
+
table.appendChild(tr);
|
|
197
|
+
});
|
|
198
|
+
div.appendChild(table);
|
|
199
|
+
}
|
|
200
|
+
uiContainer.appendChild(div);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
select.onchange = (e) => renderPage(e.target.value);
|
|
205
|
+
const initialPage = select.value || (pages[0] ? pages[0].name : "");
|
|
206
|
+
if (initialPage) {
|
|
207
|
+
renderPage(initialPage);
|
|
208
|
+
} else {
|
|
209
|
+
uiContainer.textContent = "No pages";
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function showEditField(element, pageName, op) {
|
|
214
|
+
const newValue = prompt("Enter new value", element.value || element.label || "");
|
|
215
|
+
if (newValue === null) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
performEdit(op, element.element_id, pageName, newValue);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function refreshAll() {
|
|
222
|
+
const [summary, ui, actions, lint] = await Promise.all([
|
|
223
|
+
fetchJson("/api/summary"),
|
|
224
|
+
fetchJson("/api/ui"),
|
|
225
|
+
fetchJson("/api/actions"),
|
|
226
|
+
fetchJson("/api/lint"),
|
|
227
|
+
]);
|
|
228
|
+
renderSummary(summary);
|
|
229
|
+
renderActions(actions);
|
|
230
|
+
renderLint(lint);
|
|
231
|
+
renderState({});
|
|
232
|
+
renderTraces([]);
|
|
233
|
+
if (ui.ok !== false) {
|
|
234
|
+
renderUI(ui);
|
|
235
|
+
} else {
|
|
236
|
+
document.getElementById("ui").textContent = ui.error || "Error";
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
document.getElementById("refresh").onclick = refreshAll;
|
|
241
|
+
document.getElementById("reset").onclick = async () => {
|
|
242
|
+
await fetch("/api/reset", { method: "POST", body: "{}" });
|
|
243
|
+
renderState({});
|
|
244
|
+
renderTraces([]);
|
|
245
|
+
refreshAll();
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
refreshAll();
|
|
@@ -0,0 +1,44 @@
|
|
|
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>Namel3ss Studio</title>
|
|
7
|
+
<link rel="stylesheet" href="/styles.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<header>
|
|
11
|
+
<h1>Namel3ss Studio</h1>
|
|
12
|
+
<button id="refresh">Refresh</button>
|
|
13
|
+
<button id="reset">Reset</button>
|
|
14
|
+
</header>
|
|
15
|
+
<main>
|
|
16
|
+
<section>
|
|
17
|
+
<h2>Summary</h2>
|
|
18
|
+
<pre id="summary"></pre>
|
|
19
|
+
</section>
|
|
20
|
+
<section>
|
|
21
|
+
<h2>UI Preview</h2>
|
|
22
|
+
<select id="pageSelect"></select>
|
|
23
|
+
<div id="ui"></div>
|
|
24
|
+
</section>
|
|
25
|
+
<section>
|
|
26
|
+
<h2>State</h2>
|
|
27
|
+
<pre id="state"></pre>
|
|
28
|
+
</section>
|
|
29
|
+
<section>
|
|
30
|
+
<h2>Traces</h2>
|
|
31
|
+
<pre id="traces"></pre>
|
|
32
|
+
</section>
|
|
33
|
+
<section>
|
|
34
|
+
<h2>Actions</h2>
|
|
35
|
+
<pre id="actions"></pre>
|
|
36
|
+
</section>
|
|
37
|
+
<section>
|
|
38
|
+
<h2>Lint Findings</h2>
|
|
39
|
+
<pre id="lint"></pre>
|
|
40
|
+
</section>
|
|
41
|
+
</main>
|
|
42
|
+
<script src="/app.js"></script>
|
|
43
|
+
</body>
|
|
44
|
+
</html>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
body {
|
|
2
|
+
font-family: Arial, sans-serif;
|
|
3
|
+
margin: 0;
|
|
4
|
+
padding: 0 16px 32px 16px;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
header {
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
gap: 12px;
|
|
11
|
+
border-bottom: 1px solid #ddd;
|
|
12
|
+
padding: 12px 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
main {
|
|
16
|
+
display: grid;
|
|
17
|
+
grid-template-columns: 1fr 1fr;
|
|
18
|
+
gap: 16px;
|
|
19
|
+
margin-top: 16px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
section {
|
|
23
|
+
border: 1px solid #eee;
|
|
24
|
+
padding: 12px;
|
|
25
|
+
border-radius: 6px;
|
|
26
|
+
background: #fafafa;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
pre {
|
|
30
|
+
background: #fff;
|
|
31
|
+
border: 1px solid #eee;
|
|
32
|
+
padding: 8px;
|
|
33
|
+
overflow: auto;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.element {
|
|
37
|
+
margin-bottom: 8px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
button {
|
|
41
|
+
padding: 6px 12px;
|
|
42
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.env
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# {{PROJECT_NAME}} (AI Assistant)
|
|
2
|
+
|
|
3
|
+
Ask an AI assistant over saved notes. Includes an echo tool, memory config, and a notes page with a call-to-action button.
|
|
4
|
+
|
|
5
|
+
Run it with:
|
|
6
|
+
- `n3 app.ai check`
|
|
7
|
+
- `n3 app.ai studio`
|
|
8
|
+
- `n3 app.ai actions`
|
|
9
|
+
|
|
10
|
+
Put any API keys in a local `.env` (not committed).
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
tool "echo":
|
|
2
|
+
kind is "builtin"
|
|
3
|
+
|
|
4
|
+
record "Note":
|
|
5
|
+
title string must be present
|
|
6
|
+
body string must be present
|
|
7
|
+
|
|
8
|
+
ai "assistant":
|
|
9
|
+
model is "gpt-4.1"
|
|
10
|
+
system_prompt is "You are a helpful assistant for {{PROJECT_NAME}}."
|
|
11
|
+
tools:
|
|
12
|
+
expose "echo"
|
|
13
|
+
memory:
|
|
14
|
+
short_term is 5
|
|
15
|
+
semantic is true
|
|
16
|
+
profile is true
|
|
17
|
+
|
|
18
|
+
flow "ask_assistant":
|
|
19
|
+
let prompt is "Summarize the latest notes."
|
|
20
|
+
ask ai "assistant" with input: prompt as reply
|
|
21
|
+
set state.reply is reply
|
|
22
|
+
return reply
|
|
23
|
+
|
|
24
|
+
page "notes":
|
|
25
|
+
title is "{{PROJECT_NAME}} Notes Assistant"
|
|
26
|
+
text is "Save notes and ask the assistant for help."
|
|
27
|
+
form is "Note"
|
|
28
|
+
table is "Note"
|
|
29
|
+
button "Ask assistant":
|
|
30
|
+
calls flow "ask_assistant"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.env
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# {{PROJECT_NAME}} (CRUD)
|
|
2
|
+
|
|
3
|
+
Starter CRUD dashboard with a customer record, seed flow, and a combined form/table page.
|
|
4
|
+
|
|
5
|
+
Run it with:
|
|
6
|
+
- `n3 app.ai check`
|
|
7
|
+
- `n3 app.ai studio`
|
|
8
|
+
- `n3 app.ai actions`
|
|
9
|
+
|
|
10
|
+
Put any API keys in a local `.env` (not committed).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
record "Customer":
|
|
2
|
+
name string must be present
|
|
3
|
+
email string must match pattern ".*@.*"
|
|
4
|
+
age int must be greater than 17
|
|
5
|
+
|
|
6
|
+
flow "seed_customers":
|
|
7
|
+
set state.customer.name is "Alice"
|
|
8
|
+
set state.customer.email is "alice@example.com"
|
|
9
|
+
set state.customer.age is 21
|
|
10
|
+
save Customer
|
|
11
|
+
set state.customer.name is "Bob"
|
|
12
|
+
set state.customer.email is "bob@example.com"
|
|
13
|
+
set state.customer.age is 30
|
|
14
|
+
save Customer
|
|
15
|
+
return "seeded"
|
|
16
|
+
|
|
17
|
+
flow "hello":
|
|
18
|
+
return "Hello from {{PROJECT_NAME}}"
|
|
19
|
+
|
|
20
|
+
page "home":
|
|
21
|
+
title is "{{PROJECT_NAME}} Dashboard"
|
|
22
|
+
text is "Add customers and view them below."
|
|
23
|
+
button "Seed demo data":
|
|
24
|
+
calls flow "seed_customers"
|
|
25
|
+
form is "Customer"
|
|
26
|
+
table is "Customer"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.env
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# {{PROJECT_NAME}} (Multi-Agent)
|
|
2
|
+
|
|
3
|
+
Planner, critic, and researcher agents collaborating on a shared assistant with a single workflow page.
|
|
4
|
+
|
|
5
|
+
Run it with:
|
|
6
|
+
- `n3 app.ai check`
|
|
7
|
+
- `n3 app.ai studio`
|
|
8
|
+
- `n3 app.ai actions`
|
|
9
|
+
|
|
10
|
+
Put any API keys in a local `.env` (not committed).
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
tool "echo":
|
|
2
|
+
kind is "builtin"
|
|
3
|
+
|
|
4
|
+
ai "assistant":
|
|
5
|
+
model is "gpt-4.1"
|
|
6
|
+
system_prompt is "You are a helpful multi-agent assistant for {{PROJECT_NAME}}."
|
|
7
|
+
tools:
|
|
8
|
+
expose "echo"
|
|
9
|
+
memory:
|
|
10
|
+
short_term is 5
|
|
11
|
+
semantic is true
|
|
12
|
+
profile is true
|
|
13
|
+
|
|
14
|
+
agent "planner":
|
|
15
|
+
ai is "assistant"
|
|
16
|
+
system_prompt is "Plan step-by-step."
|
|
17
|
+
|
|
18
|
+
agent "critic":
|
|
19
|
+
ai is "assistant"
|
|
20
|
+
system_prompt is "Find risks and gaps."
|
|
21
|
+
|
|
22
|
+
agent "researcher":
|
|
23
|
+
ai is "assistant"
|
|
24
|
+
system_prompt is "Add supporting details."
|
|
25
|
+
|
|
26
|
+
flow "run_workflow":
|
|
27
|
+
let task is "Plan a 3-step launch checklist for {{PROJECT_NAME}}"
|
|
28
|
+
run agent "planner" with input: task as plan
|
|
29
|
+
run agents in parallel:
|
|
30
|
+
agent "critic" with input: plan
|
|
31
|
+
agent "researcher" with input: plan
|
|
32
|
+
as feedback
|
|
33
|
+
ask ai "assistant" with input: plan as final
|
|
34
|
+
set state.plan is plan
|
|
35
|
+
set state.feedback is feedback
|
|
36
|
+
set state.final is final
|
|
37
|
+
return final
|
|
38
|
+
|
|
39
|
+
page "workflow":
|
|
40
|
+
title is "{{PROJECT_NAME}} Multi-Agent Workflow"
|
|
41
|
+
text is "Planner plus critic and researcher."
|
|
42
|
+
button "Run workflow":
|
|
43
|
+
calls flow "run_workflow"
|
namel3ss/ui/__init__.py
ADDED