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
namel3ss/cli/json_io.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from namel3ss.errors.base import Namel3ssError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_payload(text: str | None) -> dict:
|
|
9
|
+
if text is None or text == "":
|
|
10
|
+
return {}
|
|
11
|
+
try:
|
|
12
|
+
data = json.loads(text)
|
|
13
|
+
except json.JSONDecodeError as exc:
|
|
14
|
+
raise Namel3ssError(f"Invalid JSON payload: {exc.msg}") from exc
|
|
15
|
+
if not isinstance(data, dict):
|
|
16
|
+
raise Namel3ssError("Payload must be a JSON object")
|
|
17
|
+
return data
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def dumps_pretty(obj: object) -> str:
|
|
21
|
+
return json.dumps(obj, indent=2, ensure_ascii=False)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from namel3ss.errors.base import Namel3ssError
|
|
6
|
+
from namel3ss.errors.render import format_error
|
|
7
|
+
from namel3ss.lint.engine import lint_source
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run_lint(path_str: str, check_only: bool) -> int:
|
|
11
|
+
path = Path(path_str)
|
|
12
|
+
if path.suffix != ".ai":
|
|
13
|
+
raise Namel3ssError("Input file must have .ai extension")
|
|
14
|
+
try:
|
|
15
|
+
source = path.read_text(encoding="utf-8")
|
|
16
|
+
except FileNotFoundError as err:
|
|
17
|
+
raise Namel3ssError(f"File not found: {path}") from err
|
|
18
|
+
findings = lint_source(source)
|
|
19
|
+
output = {
|
|
20
|
+
"ok": len(findings) == 0,
|
|
21
|
+
"count": len(findings),
|
|
22
|
+
"findings": [f.to_dict() for f in findings],
|
|
23
|
+
}
|
|
24
|
+
import json
|
|
25
|
+
|
|
26
|
+
print(json.dumps(output, indent=2, ensure_ascii=False))
|
|
27
|
+
if check_only and findings:
|
|
28
|
+
return 1
|
|
29
|
+
return 0
|
namel3ss/cli/main.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from namel3ss.cli.actions_mode import list_actions
|
|
6
|
+
from namel3ss.cli.app_loader import load_program
|
|
7
|
+
from namel3ss.cli.format_mode import run_format
|
|
8
|
+
from namel3ss.cli.new_mode import run_new
|
|
9
|
+
from namel3ss.cli.lint_mode import run_lint
|
|
10
|
+
from namel3ss.cli.json_io import dumps_pretty, parse_payload
|
|
11
|
+
from namel3ss.cli.runner import run_flow
|
|
12
|
+
from namel3ss.cli.ui_mode import render_manifest, run_action
|
|
13
|
+
from namel3ss.cli.studio_mode import run_studio
|
|
14
|
+
from namel3ss.errors.base import Namel3ssError
|
|
15
|
+
from namel3ss.errors.render import format_error
|
|
16
|
+
|
|
17
|
+
RESERVED = {"check", "ui", "flow", "help", "format", "lint", "actions", "studio"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main(argv: list[str] | None = None) -> int:
|
|
21
|
+
args = sys.argv[1:] if argv is None else argv
|
|
22
|
+
try:
|
|
23
|
+
if not args:
|
|
24
|
+
_print_usage()
|
|
25
|
+
return 1
|
|
26
|
+
|
|
27
|
+
if args[0] == "help":
|
|
28
|
+
_print_usage()
|
|
29
|
+
return 0
|
|
30
|
+
if args[0] == "new":
|
|
31
|
+
return run_new(args[1:])
|
|
32
|
+
|
|
33
|
+
path = args[0]
|
|
34
|
+
remainder = args[1:]
|
|
35
|
+
|
|
36
|
+
if remainder and remainder[0] == "format":
|
|
37
|
+
check_only = len(remainder) > 1 and remainder[1] == "check"
|
|
38
|
+
return run_format(path, check_only)
|
|
39
|
+
if remainder and remainder[0] == "lint":
|
|
40
|
+
check_only = len(remainder) > 1 and remainder[1] == "check"
|
|
41
|
+
return run_lint(path, check_only)
|
|
42
|
+
if remainder and remainder[0] == "actions":
|
|
43
|
+
json_mode = len(remainder) > 1 and remainder[1] == "json"
|
|
44
|
+
program_ir, source = load_program(path)
|
|
45
|
+
json_payload, text_output = list_actions(program_ir, json_mode)
|
|
46
|
+
if json_mode:
|
|
47
|
+
print(dumps_pretty(json_payload))
|
|
48
|
+
else:
|
|
49
|
+
print(text_output or "")
|
|
50
|
+
return 0
|
|
51
|
+
if remainder and remainder[0] == "studio":
|
|
52
|
+
port = 7333
|
|
53
|
+
dry = False
|
|
54
|
+
tail = remainder[1:]
|
|
55
|
+
i = 0
|
|
56
|
+
while i < len(tail):
|
|
57
|
+
if tail[i] == "--port" and i + 1 < len(tail):
|
|
58
|
+
try:
|
|
59
|
+
port = int(tail[i + 1])
|
|
60
|
+
except ValueError:
|
|
61
|
+
raise Namel3ssError("Port must be an integer")
|
|
62
|
+
i += 2
|
|
63
|
+
continue
|
|
64
|
+
if tail[i] == "--dry":
|
|
65
|
+
dry = True
|
|
66
|
+
i += 1
|
|
67
|
+
continue
|
|
68
|
+
i += 1
|
|
69
|
+
return run_studio(path, port, dry)
|
|
70
|
+
|
|
71
|
+
program_ir, source = load_program(path)
|
|
72
|
+
if not remainder:
|
|
73
|
+
return _run_default(program_ir)
|
|
74
|
+
cmd = remainder[0]
|
|
75
|
+
tail = remainder[1:]
|
|
76
|
+
if cmd == "check":
|
|
77
|
+
print("OK")
|
|
78
|
+
return 0
|
|
79
|
+
if cmd == "ui":
|
|
80
|
+
manifest = render_manifest(program_ir)
|
|
81
|
+
print(dumps_pretty(manifest))
|
|
82
|
+
return 0
|
|
83
|
+
if cmd == "flow":
|
|
84
|
+
if not tail:
|
|
85
|
+
raise Namel3ssError('Missing flow name. Use: n3 <app.ai> flow "<name>"')
|
|
86
|
+
flow_name = tail[0]
|
|
87
|
+
output = run_flow(program_ir, flow_name)
|
|
88
|
+
print(dumps_pretty(output))
|
|
89
|
+
return 0
|
|
90
|
+
if cmd == "help":
|
|
91
|
+
_print_usage()
|
|
92
|
+
return 0
|
|
93
|
+
# action mode
|
|
94
|
+
if cmd in RESERVED:
|
|
95
|
+
raise Namel3ssError("Unexpected reserved command position")
|
|
96
|
+
action_id = cmd
|
|
97
|
+
payload_text = tail[0] if tail else "{}"
|
|
98
|
+
payload = parse_payload(payload_text)
|
|
99
|
+
response = run_action(program_ir, action_id, payload)
|
|
100
|
+
print(dumps_pretty(response))
|
|
101
|
+
return 0
|
|
102
|
+
except Namel3ssError as err:
|
|
103
|
+
print(format_error(err, locals().get("source", "")), file=sys.stderr)
|
|
104
|
+
return 1
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _run_default(program_ir) -> int:
|
|
108
|
+
output = run_flow(program_ir, None)
|
|
109
|
+
print(dumps_pretty(output))
|
|
110
|
+
return 0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _print_usage() -> None:
|
|
114
|
+
usage = """Usage:
|
|
115
|
+
n3 new [template] [name] # scaffold from a template (omit args to list)
|
|
116
|
+
n3 <app.ai> # run default flow
|
|
117
|
+
n3 <app.ai> check # validate only
|
|
118
|
+
n3 <app.ai> ui # print UI manifest
|
|
119
|
+
n3 <app.ai> flow "<name>" # run specific flow
|
|
120
|
+
n3 <app.ai> format # format in place
|
|
121
|
+
n3 <app.ai> format check # check formatting only
|
|
122
|
+
n3 <app.ai> lint # lint and print findings
|
|
123
|
+
n3 <app.ai> lint check # lint, fail on findings
|
|
124
|
+
n3 <app.ai> studio [--port N] # start Studio viewer (use --dry to skip server in tests)
|
|
125
|
+
n3 <app.ai> studio --dry # dry run (prints URL)
|
|
126
|
+
n3 <app.ai> actions # list actions (plain text)
|
|
127
|
+
n3 <app.ai> actions json # list actions (JSON)
|
|
128
|
+
n3 <app.ai> <action_id> [json] # execute UI action (payload optional)
|
|
129
|
+
n3 <app.ai> help # this help
|
|
130
|
+
"""
|
|
131
|
+
print(usage.strip())
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
if __name__ == "__main__":
|
|
135
|
+
sys.exit(main())
|
namel3ss/cli/new_mode.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import shutil
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from namel3ss.errors.base import Namel3ssError
|
|
9
|
+
from namel3ss.format import format_source
|
|
10
|
+
from namel3ss.lint.engine import lint_source
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class TemplateSpec:
|
|
15
|
+
name: str
|
|
16
|
+
directory: str
|
|
17
|
+
description: str
|
|
18
|
+
aliases: tuple[str, ...] = ()
|
|
19
|
+
|
|
20
|
+
def matches(self, candidate: str) -> bool:
|
|
21
|
+
normalized = candidate.lower().replace("_", "-")
|
|
22
|
+
if normalized == self.name:
|
|
23
|
+
return True
|
|
24
|
+
normalized_aliases = {alias.lower().replace("_", "-") for alias in self.aliases}
|
|
25
|
+
normalized_aliases.add(self.directory.lower().replace("_", "-"))
|
|
26
|
+
return normalized in normalized_aliases
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
TEMPLATES: tuple[TemplateSpec, ...] = (
|
|
30
|
+
TemplateSpec(name="crud", directory="crud", description="CRUD dashboard with form and table."),
|
|
31
|
+
TemplateSpec(
|
|
32
|
+
name="ai-assistant",
|
|
33
|
+
directory="ai_assistant",
|
|
34
|
+
description="AI assistant over records with memory and tooling.",
|
|
35
|
+
aliases=("ai_assistant",),
|
|
36
|
+
),
|
|
37
|
+
TemplateSpec(
|
|
38
|
+
name="multi-agent",
|
|
39
|
+
directory="multi_agent",
|
|
40
|
+
description="Planner, critic, and researcher agents sharing one assistant.",
|
|
41
|
+
aliases=("multi_agent",),
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def run_new(args: list[str]) -> int:
|
|
47
|
+
if not args:
|
|
48
|
+
print(render_templates_list())
|
|
49
|
+
return 0
|
|
50
|
+
if len(args) > 2:
|
|
51
|
+
raise Namel3ssError("Usage: n3 new <template> [project_name]")
|
|
52
|
+
template_name = args[0]
|
|
53
|
+
template = _resolve_template(template_name)
|
|
54
|
+
project_input = args[1] if len(args) == 2 else template.name
|
|
55
|
+
project_name = _normalize_project_name(project_input)
|
|
56
|
+
|
|
57
|
+
template_dir = _templates_root() / template.directory
|
|
58
|
+
if not template_dir.exists():
|
|
59
|
+
raise Namel3ssError(f"Template '{template.name}' is not installed (missing {template_dir}).")
|
|
60
|
+
|
|
61
|
+
target_dir = Path.cwd() / project_name
|
|
62
|
+
if target_dir.exists():
|
|
63
|
+
raise Namel3ssError(f"Directory already exists: {target_dir}")
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
shutil.copytree(template_dir, target_dir)
|
|
67
|
+
_prepare_readme(target_dir, project_name)
|
|
68
|
+
formatted_source = _prepare_app_file(target_dir, project_name)
|
|
69
|
+
except Exception:
|
|
70
|
+
shutil.rmtree(target_dir, ignore_errors=True)
|
|
71
|
+
raise
|
|
72
|
+
|
|
73
|
+
findings = lint_source(formatted_source)
|
|
74
|
+
if findings:
|
|
75
|
+
print("Lint findings:")
|
|
76
|
+
for finding in findings:
|
|
77
|
+
location = f"[line {finding.line}, col {finding.column}] " if finding.line else ""
|
|
78
|
+
print(f" - {location}{finding.code}: {finding.message}")
|
|
79
|
+
|
|
80
|
+
_print_success_message(project_name, target_dir)
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def render_templates_list() -> str:
|
|
85
|
+
longest = max(len(t.name) for t in TEMPLATES)
|
|
86
|
+
lines = ["Available templates:"]
|
|
87
|
+
for template in TEMPLATES:
|
|
88
|
+
padded = template.name.ljust(longest)
|
|
89
|
+
lines.append(f" {padded} - {template.description}")
|
|
90
|
+
return "\n".join(lines)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _templates_root() -> Path:
|
|
94
|
+
return Path(__file__).resolve().parent.parent / "templates"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _resolve_template(name: str) -> TemplateSpec:
|
|
98
|
+
for template in TEMPLATES:
|
|
99
|
+
if template.matches(name):
|
|
100
|
+
return template
|
|
101
|
+
available = ", ".join(t.name for t in TEMPLATES)
|
|
102
|
+
raise Namel3ssError(f"Unknown template '{name}'. Available templates: {available}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _normalize_project_name(name: str) -> str:
|
|
106
|
+
normalized = name.replace("-", "_")
|
|
107
|
+
normalized = re.sub(r"[^A-Za-z0-9_]+", "_", normalized).strip("_")
|
|
108
|
+
if not normalized:
|
|
109
|
+
raise Namel3ssError("Project name cannot be empty after normalization")
|
|
110
|
+
return normalized
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _prepare_readme(target_dir: Path, project_name: str) -> None:
|
|
114
|
+
readme_path = target_dir / "README.md"
|
|
115
|
+
if not readme_path.exists():
|
|
116
|
+
raise Namel3ssError(f"Template is missing README.md at {readme_path}")
|
|
117
|
+
_rewrite_with_project_name(readme_path, project_name)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _prepare_app_file(target_dir: Path, project_name: str) -> str:
|
|
121
|
+
app_path = target_dir / "app.ai"
|
|
122
|
+
if not app_path.exists():
|
|
123
|
+
raise Namel3ssError(f"Template is missing app.ai at {app_path}")
|
|
124
|
+
raw = _rewrite_with_project_name(app_path, project_name)
|
|
125
|
+
formatted = format_source(raw)
|
|
126
|
+
app_mode = app_path.stat().st_mode
|
|
127
|
+
app_path.write_text(formatted, encoding="utf-8")
|
|
128
|
+
app_path.chmod(app_mode)
|
|
129
|
+
return formatted
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _rewrite_with_project_name(path: Path, project_name: str) -> str:
|
|
133
|
+
original_mode = path.stat().st_mode
|
|
134
|
+
contents = path.read_text(encoding="utf-8")
|
|
135
|
+
updated = contents.replace("{{PROJECT_NAME}}", project_name)
|
|
136
|
+
path.write_text(updated, encoding="utf-8")
|
|
137
|
+
path.chmod(original_mode)
|
|
138
|
+
return updated
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _print_success_message(project_name: str, target_dir: Path) -> None:
|
|
142
|
+
print(f"Created project at {target_dir}")
|
|
143
|
+
print("Next steps:")
|
|
144
|
+
print(f" cd {project_name}")
|
|
145
|
+
print(" n3 app.ai studio")
|
|
146
|
+
print(" n3 app.ai actions")
|
namel3ss/cli/runner.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from namel3ss.errors.base import Namel3ssError
|
|
4
|
+
from namel3ss.runtime.executor import execute_program_flow
|
|
5
|
+
from namel3ss.runtime.store.memory_store import MemoryStore
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run_flow(program_ir, flow_name: str | None = None) -> dict:
|
|
9
|
+
selected = _select_flow(program_ir, flow_name)
|
|
10
|
+
result = execute_program_flow(program_ir, selected, state={}, input={}, store=MemoryStore())
|
|
11
|
+
traces = [_trace_to_dict(t) for t in result.traces]
|
|
12
|
+
return {"ok": True, "state": result.state, "result": result.last_value, "traces": traces}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _select_flow(program_ir, flow_name: str | None) -> str:
|
|
16
|
+
if flow_name:
|
|
17
|
+
return flow_name
|
|
18
|
+
if len(program_ir.flows) == 1:
|
|
19
|
+
return program_ir.flows[0].name
|
|
20
|
+
raise Namel3ssError('Multiple flows found; use: n3 <app.ai> flow "<name>"')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _trace_to_dict(trace) -> dict:
|
|
24
|
+
if hasattr(trace, "__dict__"):
|
|
25
|
+
return trace.__dict__
|
|
26
|
+
if isinstance(trace, dict):
|
|
27
|
+
return trace
|
|
28
|
+
return {"trace": trace}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from namel3ss.cli.app_loader import load_program
|
|
6
|
+
from namel3ss.errors.base import Namel3ssError
|
|
7
|
+
from namel3ss.errors.render import format_error
|
|
8
|
+
from namel3ss.studio.server import start_server
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run_studio(path: str, port: int, dry: bool) -> int:
|
|
12
|
+
source = ""
|
|
13
|
+
try:
|
|
14
|
+
program_ir, source = load_program(path)
|
|
15
|
+
if dry:
|
|
16
|
+
print(f"Studio: http://127.0.0.1:{port}/")
|
|
17
|
+
return 0
|
|
18
|
+
start_server(path, port)
|
|
19
|
+
return 0
|
|
20
|
+
except Namel3ssError as err:
|
|
21
|
+
print(format_error(err, source), file=sys.stderr)
|
|
22
|
+
return 1
|
namel3ss/cli/ui_mode.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from namel3ss.errors.base import Namel3ssError
|
|
4
|
+
from namel3ss.runtime.store.memory_store import MemoryStore
|
|
5
|
+
from namel3ss.runtime.ui.actions import handle_action
|
|
6
|
+
from namel3ss.ui.manifest import build_manifest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def render_manifest(program_ir) -> dict:
|
|
10
|
+
return build_manifest(program_ir, state={}, store=MemoryStore())
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run_action(program_ir, action_id: str, payload: dict) -> dict:
|
|
14
|
+
return handle_action(program_ir, action_id=action_id, payload=payload, state={}, store=MemoryStore())
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def load_dotenv_for_path(ai_path: str) -> dict[str, str]:
|
|
8
|
+
path = Path(ai_path).resolve()
|
|
9
|
+
env_path = path.parent / ".env"
|
|
10
|
+
if not env_path.exists():
|
|
11
|
+
return {}
|
|
12
|
+
values: dict[str, str] = {}
|
|
13
|
+
for line in env_path.read_text(encoding="utf-8").splitlines():
|
|
14
|
+
stripped = line.strip()
|
|
15
|
+
if not stripped or stripped.startswith("#"):
|
|
16
|
+
continue
|
|
17
|
+
if "=" not in stripped:
|
|
18
|
+
continue
|
|
19
|
+
key, raw_value = stripped.split("=", 1)
|
|
20
|
+
key = key.strip()
|
|
21
|
+
value = raw_value.strip().strip('"').strip("'")
|
|
22
|
+
if key:
|
|
23
|
+
values[key] = value
|
|
24
|
+
return values
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def apply_dotenv(values: dict[str, str]) -> None:
|
|
28
|
+
for key, value in values.items():
|
|
29
|
+
if key not in os.environ:
|
|
30
|
+
os.environ[key] = value
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = ["load_dotenv_for_path", "apply_dotenv"]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from namel3ss.config.model import AppConfig
|
|
8
|
+
from namel3ss.errors.base import Namel3ssError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
CONFIG_PATH = Path.home() / ".namel3ss" / "config.json"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_config(config_path: Path | None = None) -> AppConfig:
|
|
15
|
+
config = AppConfig()
|
|
16
|
+
path = config_path or CONFIG_PATH
|
|
17
|
+
if path.exists():
|
|
18
|
+
_apply_file_config(config, path)
|
|
19
|
+
_apply_env_overrides(config)
|
|
20
|
+
return config
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _apply_file_config(config: AppConfig, path: Path) -> None:
|
|
24
|
+
try:
|
|
25
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
26
|
+
except json.JSONDecodeError as err:
|
|
27
|
+
raise Namel3ssError(f"Invalid config file at {path}: {err}") from err
|
|
28
|
+
if not isinstance(data, dict):
|
|
29
|
+
raise Namel3ssError(f"Config file must contain an object at {path}")
|
|
30
|
+
ollama_cfg = data.get("ollama", {})
|
|
31
|
+
if isinstance(ollama_cfg, dict):
|
|
32
|
+
if "host" in ollama_cfg:
|
|
33
|
+
config.ollama.host = str(ollama_cfg["host"])
|
|
34
|
+
if "timeout_seconds" in ollama_cfg:
|
|
35
|
+
try:
|
|
36
|
+
config.ollama.timeout_seconds = int(ollama_cfg["timeout_seconds"])
|
|
37
|
+
except (TypeError, ValueError) as err:
|
|
38
|
+
raise Namel3ssError("ollama.timeout_seconds must be an integer") from err
|
|
39
|
+
openai_cfg = data.get("openai", {})
|
|
40
|
+
if isinstance(openai_cfg, dict):
|
|
41
|
+
if "api_key" in openai_cfg:
|
|
42
|
+
config.openai.api_key = str(openai_cfg["api_key"])
|
|
43
|
+
if "base_url" in openai_cfg:
|
|
44
|
+
config.openai.base_url = str(openai_cfg["base_url"])
|
|
45
|
+
anthropic_cfg = data.get("anthropic", {})
|
|
46
|
+
if isinstance(anthropic_cfg, dict) and "api_key" in anthropic_cfg:
|
|
47
|
+
config.anthropic.api_key = str(anthropic_cfg["api_key"])
|
|
48
|
+
gemini_cfg = data.get("gemini", {})
|
|
49
|
+
if isinstance(gemini_cfg, dict) and "api_key" in gemini_cfg:
|
|
50
|
+
config.gemini.api_key = str(gemini_cfg["api_key"])
|
|
51
|
+
mistral_cfg = data.get("mistral", {})
|
|
52
|
+
if isinstance(mistral_cfg, dict) and "api_key" in mistral_cfg:
|
|
53
|
+
config.mistral.api_key = str(mistral_cfg["api_key"])
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _apply_env_overrides(config: AppConfig) -> None:
|
|
57
|
+
host = os.getenv("NAMEL3SS_OLLAMA_HOST")
|
|
58
|
+
if host:
|
|
59
|
+
config.ollama.host = host
|
|
60
|
+
timeout = os.getenv("NAMEL3SS_OLLAMA_TIMEOUT_SECONDS")
|
|
61
|
+
if timeout:
|
|
62
|
+
try:
|
|
63
|
+
config.ollama.timeout_seconds = int(timeout)
|
|
64
|
+
except ValueError as err:
|
|
65
|
+
raise Namel3ssError("NAMEL3SS_OLLAMA_TIMEOUT_SECONDS must be an integer") from err
|
|
66
|
+
api_key = os.getenv("NAMEL3SS_OPENAI_API_KEY")
|
|
67
|
+
if api_key:
|
|
68
|
+
config.openai.api_key = api_key
|
|
69
|
+
base_url = os.getenv("NAMEL3SS_OPENAI_BASE_URL")
|
|
70
|
+
if base_url:
|
|
71
|
+
config.openai.base_url = base_url
|
|
72
|
+
anthropic_key = os.getenv("NAMEL3SS_ANTHROPIC_API_KEY")
|
|
73
|
+
if anthropic_key:
|
|
74
|
+
config.anthropic.api_key = anthropic_key
|
|
75
|
+
gemini_key = os.getenv("NAMEL3SS_GEMINI_API_KEY")
|
|
76
|
+
if gemini_key:
|
|
77
|
+
config.gemini.api_key = gemini_key
|
|
78
|
+
mistral_key = os.getenv("NAMEL3SS_MISTRAL_API_KEY")
|
|
79
|
+
if mistral_key:
|
|
80
|
+
config.mistral.api_key = mistral_key
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
__all__ = ["load_config", "CONFIG_PATH"]
|
namel3ss/config/model.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class OllamaConfig:
|
|
8
|
+
host: str = "http://127.0.0.1:11434"
|
|
9
|
+
timeout_seconds: int = 30
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class OpenAIConfig:
|
|
14
|
+
api_key: str | None = None
|
|
15
|
+
base_url: str = "https://api.openai.com"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class AnthropicConfig:
|
|
20
|
+
api_key: str | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class GeminiConfig:
|
|
25
|
+
api_key: str | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class MistralConfig:
|
|
30
|
+
api_key: str | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class AppConfig:
|
|
35
|
+
ollama: OllamaConfig = field(default_factory=OllamaConfig)
|
|
36
|
+
openai: OpenAIConfig = field(default_factory=OpenAIConfig)
|
|
37
|
+
anthropic: AnthropicConfig = field(default_factory=AnthropicConfig)
|
|
38
|
+
gemini: GeminiConfig = field(default_factory=GeminiConfig)
|
|
39
|
+
mistral: MistralConfig = field(default_factory=MistralConfig)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"AppConfig",
|
|
44
|
+
"OllamaConfig",
|
|
45
|
+
"OpenAIConfig",
|
|
46
|
+
"AnthropicConfig",
|
|
47
|
+
"GeminiConfig",
|
|
48
|
+
"MistralConfig",
|
|
49
|
+
]
|
namel3ss/errors/base.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared error types for Namel3ss.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Namel3ssError(Exception):
|
|
13
|
+
"""Base error with optional source location."""
|
|
14
|
+
|
|
15
|
+
message: str
|
|
16
|
+
line: Optional[int] = None
|
|
17
|
+
column: Optional[int] = None
|
|
18
|
+
end_line: Optional[int] = None
|
|
19
|
+
end_column: Optional[int] = None
|
|
20
|
+
|
|
21
|
+
def __str__(self) -> str:
|
|
22
|
+
location = self._format_location()
|
|
23
|
+
return f"{location}{self.message}" if location else self.message
|
|
24
|
+
|
|
25
|
+
def _format_location(self) -> str:
|
|
26
|
+
if self.line is None:
|
|
27
|
+
return ""
|
|
28
|
+
if self.end_line is None or self.end_column is None:
|
|
29
|
+
return f"[line {self.line}, col {self.column or 1}] "
|
|
30
|
+
return (
|
|
31
|
+
f"[line {self.line}, col {self.column or 1} - "
|
|
32
|
+
f"line {self.end_line}, col {self.end_column}] "
|
|
33
|
+
)
|
|
34
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from namel3ss.errors.base import Namel3ssError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def format_error(err: Namel3ssError, source: Optional[str] = None) -> str:
|
|
9
|
+
base = str(err)
|
|
10
|
+
if source is None or err.line is None:
|
|
11
|
+
return base
|
|
12
|
+
|
|
13
|
+
lines = source.splitlines()
|
|
14
|
+
line_index = err.line - 1
|
|
15
|
+
if line_index < 0 or line_index >= len(lines):
|
|
16
|
+
return base
|
|
17
|
+
|
|
18
|
+
line_text = lines[line_index]
|
|
19
|
+
column = err.column if err.column is not None else 1
|
|
20
|
+
caret_pos = max(1, min(column, len(line_text) + 1))
|
|
21
|
+
caret_line = " " * (caret_pos - 1) + "^"
|
|
22
|
+
return f"{base}\n{line_text}\n{caret_line}"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from namel3ss.format.rules import collapse_blank_lines, migrate_buttons, normalize_indentation, normalize_spacing
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def format_source(source: str) -> str:
|
|
7
|
+
lines = source.splitlines()
|
|
8
|
+
lines = [line.rstrip() for line in lines]
|
|
9
|
+
lines = migrate_buttons(lines)
|
|
10
|
+
lines = [normalize_spacing(line) for line in lines]
|
|
11
|
+
lines = normalize_indentation(lines)
|
|
12
|
+
lines = collapse_blank_lines(lines)
|
|
13
|
+
formatted = "\n".join(lines)
|
|
14
|
+
if formatted and not formatted.endswith("\n"):
|
|
15
|
+
formatted += "\n"
|
|
16
|
+
if not formatted and source.endswith("\n"):
|
|
17
|
+
formatted = "\n"
|
|
18
|
+
return formatted
|