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,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import List, Set
|
|
4
|
+
|
|
5
|
+
from namel3ss.ir import nodes as ir
|
|
6
|
+
from namel3ss.lint.types import Finding
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def lint_semantic(program_ir: ir.Program) -> List[Finding]:
|
|
10
|
+
findings: List[Finding] = []
|
|
11
|
+
flow_names: Set[str] = {flow.name for flow in program_ir.flows}
|
|
12
|
+
record_names: Set[str] = {record.name for record in program_ir.records}
|
|
13
|
+
for page in program_ir.pages:
|
|
14
|
+
for item in page.items:
|
|
15
|
+
if isinstance(item, ir.ButtonItem):
|
|
16
|
+
if item.flow_name not in flow_names:
|
|
17
|
+
findings.append(
|
|
18
|
+
Finding(
|
|
19
|
+
code="refs.unknown_flow",
|
|
20
|
+
message=f"Button references unknown flow '{item.flow_name}'",
|
|
21
|
+
line=item.line,
|
|
22
|
+
column=item.column,
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
if isinstance(item, ir.FormItem):
|
|
26
|
+
if item.record_name not in record_names:
|
|
27
|
+
findings.append(
|
|
28
|
+
Finding(
|
|
29
|
+
code="refs.unknown_record",
|
|
30
|
+
message=f"Form references unknown record '{item.record_name}'",
|
|
31
|
+
line=item.line,
|
|
32
|
+
column=item.column,
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
if isinstance(item, ir.TableItem):
|
|
36
|
+
if item.record_name not in record_names:
|
|
37
|
+
findings.append(
|
|
38
|
+
Finding(
|
|
39
|
+
code="refs.unknown_record",
|
|
40
|
+
message=f"Table references unknown record '{item.record_name}'",
|
|
41
|
+
line=item.line,
|
|
42
|
+
column=item.column,
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
return findings
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
from namel3ss.lint.types import Finding
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
LEGACY_DECL = re.compile(r'^(flow|page|record|ai|agent|tool)\s+is\s+"')
|
|
10
|
+
ONE_LINE_BUTTON = re.compile(r'^\s*button\s+"[^"]+"\s+calls\s+flow\s+"[^"]+"\s*$')
|
|
11
|
+
PAGE_HEADER = re.compile(r'^\s*page\s+"[^"]+"\s*:\s*$')
|
|
12
|
+
FORBIDDEN_PAGE_TOKENS = {
|
|
13
|
+
"let",
|
|
14
|
+
"set",
|
|
15
|
+
"if",
|
|
16
|
+
"match",
|
|
17
|
+
"repeat",
|
|
18
|
+
"for",
|
|
19
|
+
"try",
|
|
20
|
+
"return",
|
|
21
|
+
"ask",
|
|
22
|
+
"run",
|
|
23
|
+
"save",
|
|
24
|
+
"find",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def scan_text(lines: List[str]) -> List[Finding]:
|
|
29
|
+
findings: List[Finding] = []
|
|
30
|
+
page_indent = None
|
|
31
|
+
for idx, line in enumerate(lines, start=1):
|
|
32
|
+
if LEGACY_DECL.search(line):
|
|
33
|
+
findings.append(
|
|
34
|
+
Finding(
|
|
35
|
+
code="grammar.decl_uses_is",
|
|
36
|
+
message="Declaration uses 'is'; use: <keyword> \"name\":",
|
|
37
|
+
line=idx,
|
|
38
|
+
column=1,
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
if ONE_LINE_BUTTON.search(line):
|
|
42
|
+
findings.append(
|
|
43
|
+
Finding(
|
|
44
|
+
code="ui.button_one_line_forbidden",
|
|
45
|
+
message='Buttons must use a block form: button "X": NEWLINE indent calls flow "Y"',
|
|
46
|
+
line=idx,
|
|
47
|
+
column=1,
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
if PAGE_HEADER.match(line):
|
|
51
|
+
page_indent = len(line) - len(line.lstrip(" "))
|
|
52
|
+
continue
|
|
53
|
+
if page_indent is not None:
|
|
54
|
+
if line.strip() == "":
|
|
55
|
+
continue
|
|
56
|
+
current_indent = len(line) - len(line.lstrip(" "))
|
|
57
|
+
if current_indent <= page_indent:
|
|
58
|
+
page_indent = None
|
|
59
|
+
continue
|
|
60
|
+
token = line.strip().split()[0].lower()
|
|
61
|
+
if token in FORBIDDEN_PAGE_TOKENS:
|
|
62
|
+
findings.append(
|
|
63
|
+
Finding(
|
|
64
|
+
code="page.imperative_not_allowed",
|
|
65
|
+
message=f"Pages are declarative only; '{token}' is not allowed inside page blocks.",
|
|
66
|
+
line=idx,
|
|
67
|
+
column=current_indent + 1,
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
return findings
|
namel3ss/lint/types.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Finding:
|
|
9
|
+
code: str
|
|
10
|
+
message: str
|
|
11
|
+
line: Optional[int]
|
|
12
|
+
column: Optional[int]
|
|
13
|
+
severity: str = "error"
|
|
14
|
+
|
|
15
|
+
def to_dict(self) -> dict:
|
|
16
|
+
return {
|
|
17
|
+
"code": self.code,
|
|
18
|
+
"message": self.message,
|
|
19
|
+
"line": self.line,
|
|
20
|
+
"column": self.column,
|
|
21
|
+
"severity": self.severity,
|
|
22
|
+
}
|
namel3ss/parser/agent.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from namel3ss.ast import nodes as ast
|
|
4
|
+
from namel3ss.errors.base import Namel3ssError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def parse_agent_decl(parser) -> ast.AgentDecl:
|
|
8
|
+
agent_tok = parser._advance()
|
|
9
|
+
name_tok = parser._expect("STRING", "Expected agent name string")
|
|
10
|
+
parser._expect("COLON", "Expected ':' after agent name")
|
|
11
|
+
parser._expect("NEWLINE", "Expected newline after agent header")
|
|
12
|
+
parser._expect("INDENT", "Expected indented agent body")
|
|
13
|
+
ai_name = None
|
|
14
|
+
system_prompt = None
|
|
15
|
+
while parser._current().type != "DEDENT":
|
|
16
|
+
if parser._match("NEWLINE"):
|
|
17
|
+
continue
|
|
18
|
+
key_tok = parser._current()
|
|
19
|
+
if key_tok.type == "AI":
|
|
20
|
+
parser._advance()
|
|
21
|
+
parser._expect("IS", "Expected 'is' after ai")
|
|
22
|
+
ai_tok = parser._expect("STRING", "Expected AI profile name")
|
|
23
|
+
ai_name = ai_tok.value
|
|
24
|
+
elif key_tok.type == "SYSTEM_PROMPT":
|
|
25
|
+
parser._advance()
|
|
26
|
+
parser._expect("IS", "Expected 'is' after system_prompt")
|
|
27
|
+
sp_tok = parser._expect("STRING", "Expected system_prompt string")
|
|
28
|
+
system_prompt = sp_tok.value
|
|
29
|
+
else:
|
|
30
|
+
raise Namel3ssError("Unknown field in agent declaration", line=key_tok.line, column=key_tok.column)
|
|
31
|
+
parser._match("NEWLINE")
|
|
32
|
+
parser._expect("DEDENT", "Expected end of agent body")
|
|
33
|
+
if ai_name is None:
|
|
34
|
+
raise Namel3ssError("Agent requires an AI profile", line=agent_tok.line, column=agent_tok.column)
|
|
35
|
+
return ast.AgentDecl(name=name_tok.value, ai_name=ai_name, system_prompt=system_prompt, line=agent_tok.line, column=agent_tok.column)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_run_agent_stmt(parser) -> ast.RunAgentStmt:
|
|
39
|
+
run_tok = parser._advance()
|
|
40
|
+
if not parser._match("AGENT"):
|
|
41
|
+
raise Namel3ssError("Expected 'agent' after run", line=run_tok.line, column=run_tok.column)
|
|
42
|
+
name_tok = parser._expect("STRING", "Expected agent name string")
|
|
43
|
+
parser._expect("WITH", "Expected 'with' in run agent")
|
|
44
|
+
parser._expect("INPUT", "Expected 'input' in run agent")
|
|
45
|
+
parser._expect("COLON", "Expected ':' after input")
|
|
46
|
+
input_expr = parser._parse_expression()
|
|
47
|
+
parser._expect("AS", "Expected 'as' to bind agent result")
|
|
48
|
+
target_tok = parser._expect("IDENT", "Expected target identifier after 'as'")
|
|
49
|
+
return ast.RunAgentStmt(agent_name=name_tok.value, input_expr=input_expr, target=target_tok.value, line=run_tok.line, column=run_tok.column)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parse_run_agents_parallel(parser) -> ast.RunAgentsParallelStmt:
|
|
53
|
+
run_tok = parser._advance()
|
|
54
|
+
if not parser._match("AGENTS"):
|
|
55
|
+
raise Namel3ssError("Expected 'agents' after run", line=run_tok.line, column=run_tok.column)
|
|
56
|
+
parser._expect("IN", "Expected 'in'")
|
|
57
|
+
parser._expect("PARALLEL", "Expected 'parallel'")
|
|
58
|
+
parser._expect("COLON", "Expected ':' after parallel header")
|
|
59
|
+
parser._expect("NEWLINE", "Expected newline after parallel header")
|
|
60
|
+
parser._expect("INDENT", "Expected indented parallel block")
|
|
61
|
+
entries: list[ast.ParallelAgentEntry] = []
|
|
62
|
+
while parser._current().type != "DEDENT":
|
|
63
|
+
if parser._match("NEWLINE"):
|
|
64
|
+
continue
|
|
65
|
+
parser._expect("AGENT", "Expected 'agent' in parallel block")
|
|
66
|
+
name_tok = parser._expect("STRING", "Expected agent name string")
|
|
67
|
+
parser._expect("WITH", "Expected 'with' in agent entry")
|
|
68
|
+
parser._expect("INPUT", "Expected 'input' in agent entry")
|
|
69
|
+
parser._expect("COLON", "Expected ':' after input")
|
|
70
|
+
input_expr = parser._parse_expression()
|
|
71
|
+
entries.append(ast.ParallelAgentEntry(agent_name=name_tok.value, input_expr=input_expr, line=name_tok.line, column=name_tok.column))
|
|
72
|
+
parser._match("NEWLINE")
|
|
73
|
+
parser._expect("DEDENT", "Expected end of parallel agents block")
|
|
74
|
+
if not entries:
|
|
75
|
+
raise Namel3ssError("Parallel agent block requires at least one entry", line=run_tok.line, column=run_tok.column)
|
|
76
|
+
parser._expect("AS", "Expected 'as' after parallel block")
|
|
77
|
+
target_tok = parser._expect("IDENT", "Expected target identifier after 'as'")
|
|
78
|
+
return ast.RunAgentsParallelStmt(entries=entries, target=target_tok.value, line=run_tok.line, column=run_tok.column)
|
namel3ss/parser/ai.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from namel3ss.ast import nodes as ast_nodes
|
|
4
|
+
from namel3ss.errors.base import Namel3ssError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def parse_ai_decl(parser) -> ast_nodes.AIDecl:
|
|
8
|
+
ai_tok = parser._advance()
|
|
9
|
+
name_tok = parser._expect("STRING", "Expected AI name string")
|
|
10
|
+
parser._expect("COLON", "Expected ':' after AI name")
|
|
11
|
+
parser._expect("NEWLINE", "Expected newline after AI header")
|
|
12
|
+
parser._expect("INDENT", "Expected indented AI body")
|
|
13
|
+
model = None
|
|
14
|
+
provider = None
|
|
15
|
+
system_prompt = None
|
|
16
|
+
exposed_tools: list[str] = []
|
|
17
|
+
memory = ast_nodes.AIMemory(line=ai_tok.line, column=ai_tok.column)
|
|
18
|
+
while parser._current().type != "DEDENT":
|
|
19
|
+
if parser._match("NEWLINE"):
|
|
20
|
+
continue
|
|
21
|
+
key_tok = parser._current()
|
|
22
|
+
if key_tok.type == "MODEL":
|
|
23
|
+
parser._advance()
|
|
24
|
+
parser._expect("IS", "Expected 'is' after model")
|
|
25
|
+
value_tok = parser._expect("STRING", "Expected model string")
|
|
26
|
+
model = value_tok.value
|
|
27
|
+
elif key_tok.type == "PROVIDER":
|
|
28
|
+
parser._advance()
|
|
29
|
+
parser._expect("IS", "Expected 'is' after provider")
|
|
30
|
+
value_tok = parser._expect("STRING", "Expected provider string")
|
|
31
|
+
provider = value_tok.value
|
|
32
|
+
elif key_tok.type == "SYSTEM_PROMPT":
|
|
33
|
+
parser._advance()
|
|
34
|
+
parser._expect("IS", "Expected 'is' after system_prompt")
|
|
35
|
+
value_tok = parser._expect("STRING", "Expected system_prompt string")
|
|
36
|
+
system_prompt = value_tok.value
|
|
37
|
+
elif key_tok.type == "TOOLS":
|
|
38
|
+
parser._advance()
|
|
39
|
+
parser._expect("COLON", "Expected ':' after tools")
|
|
40
|
+
parser._expect("NEWLINE", "Expected newline after tools:")
|
|
41
|
+
parser._expect("INDENT", "Expected indented tools block")
|
|
42
|
+
while parser._current().type != "DEDENT":
|
|
43
|
+
if parser._match("NEWLINE"):
|
|
44
|
+
continue
|
|
45
|
+
if parser._match("EXPOSE"):
|
|
46
|
+
tool_tok = parser._expect("STRING", "Expected tool name string")
|
|
47
|
+
tool_name = tool_tok.value
|
|
48
|
+
if tool_name in exposed_tools:
|
|
49
|
+
raise Namel3ssError(f"Duplicate tool exposure '{tool_name}'", line=tool_tok.line, column=tool_tok.column)
|
|
50
|
+
exposed_tools.append(tool_name)
|
|
51
|
+
else:
|
|
52
|
+
raise Namel3ssError("Unknown entry in tools block", line=parser._current().line, column=parser._current().column)
|
|
53
|
+
parser._match("NEWLINE")
|
|
54
|
+
parser._expect("DEDENT", "Expected end of tools block")
|
|
55
|
+
elif key_tok.type == "MEMORY":
|
|
56
|
+
parser._advance()
|
|
57
|
+
parser._expect("COLON", "Expected ':' after memory")
|
|
58
|
+
parser._expect("NEWLINE", "Expected newline after memory:")
|
|
59
|
+
parser._expect("INDENT", "Expected indented memory block")
|
|
60
|
+
while parser._current().type != "DEDENT":
|
|
61
|
+
if parser._match("NEWLINE"):
|
|
62
|
+
continue
|
|
63
|
+
mem_key = parser._current()
|
|
64
|
+
if mem_key.type == "SHORT_TERM":
|
|
65
|
+
parser._advance()
|
|
66
|
+
parser._expect("IS", "Expected 'is' after short_term")
|
|
67
|
+
value_tok = parser._expect("NUMBER", "short_term must be a number literal")
|
|
68
|
+
if not isinstance(value_tok.value, int) or value_tok.value < 0:
|
|
69
|
+
raise Namel3ssError("short_term must be a non-negative integer", line=value_tok.line, column=value_tok.column)
|
|
70
|
+
memory.short_term = value_tok.value
|
|
71
|
+
elif mem_key.type == "SEMANTIC":
|
|
72
|
+
parser._advance()
|
|
73
|
+
parser._expect("IS", "Expected 'is' after semantic")
|
|
74
|
+
bool_tok = parser._expect("BOOLEAN", "semantic must be true/false literal")
|
|
75
|
+
memory.semantic = bool_tok.value
|
|
76
|
+
elif mem_key.type == "PROFILE":
|
|
77
|
+
parser._advance()
|
|
78
|
+
parser._expect("IS", "Expected 'is' after profile")
|
|
79
|
+
bool_tok = parser._expect("BOOLEAN", "profile must be true/false literal")
|
|
80
|
+
memory.profile = bool_tok.value
|
|
81
|
+
else:
|
|
82
|
+
raise Namel3ssError("Unknown memory field", line=mem_key.line, column=mem_key.column)
|
|
83
|
+
parser._match("NEWLINE")
|
|
84
|
+
parser._expect("DEDENT", "Expected end of memory block")
|
|
85
|
+
else:
|
|
86
|
+
raise Namel3ssError("Unknown field in AI declaration", line=key_tok.line, column=key_tok.column)
|
|
87
|
+
parser._match("NEWLINE")
|
|
88
|
+
parser._expect("DEDENT", "Expected end of AI body")
|
|
89
|
+
if model is None:
|
|
90
|
+
raise Namel3ssError("AI declaration requires a model", line=ai_tok.line, column=ai_tok.column)
|
|
91
|
+
return ast_nodes.AIDecl(
|
|
92
|
+
name=name_tok.value,
|
|
93
|
+
model=model,
|
|
94
|
+
provider=provider,
|
|
95
|
+
system_prompt=system_prompt,
|
|
96
|
+
exposed_tools=exposed_tools,
|
|
97
|
+
memory=memory,
|
|
98
|
+
line=ai_tok.line,
|
|
99
|
+
column=ai_tok.column,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def parse_ask_stmt(parser) -> ast_nodes.AskAIStmt:
|
|
104
|
+
ask_tok = parser._advance()
|
|
105
|
+
parser._expect("AI", "Expected 'ai' after 'ask'")
|
|
106
|
+
name_tok = parser._expect("STRING", "Expected AI name string")
|
|
107
|
+
parser._expect("WITH", "Expected 'with' in ask ai statement")
|
|
108
|
+
parser._expect("INPUT", "Expected 'input' in ask ai statement")
|
|
109
|
+
parser._expect("COLON", "Expected ':' after input")
|
|
110
|
+
input_expr = parser._parse_expression()
|
|
111
|
+
parser._expect("AS", "Expected 'as' to bind AI result")
|
|
112
|
+
target_tok = parser._expect("IDENT", "Expected target identifier after 'as'")
|
|
113
|
+
return ast_nodes.AskAIStmt(ai_name=name_tok.value, input_expr=input_expr, target=target_tok.value, line=ask_tok.line, column=ask_tok.column)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from namel3ss.ast import nodes as ast
|
|
4
|
+
from namel3ss.errors.base import Namel3ssError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def parse_field_constraint(parser) -> ast.FieldConstraint:
|
|
8
|
+
tok = parser._current()
|
|
9
|
+
if parser._match("BE"):
|
|
10
|
+
if parser._match("PRESENT"):
|
|
11
|
+
return ast.FieldConstraint(kind="present", line=tok.line, column=tok.column)
|
|
12
|
+
if parser._match("UNIQUE"):
|
|
13
|
+
return ast.FieldConstraint(kind="unique", line=tok.line, column=tok.column)
|
|
14
|
+
if parser._match("GREATER"):
|
|
15
|
+
parser._expect("THAN", "Expected 'than' after 'greater'")
|
|
16
|
+
expr = parser._parse_expression()
|
|
17
|
+
return ast.FieldConstraint(kind="gt", expression=expr, line=tok.line, column=tok.column)
|
|
18
|
+
if parser._match("LESS"):
|
|
19
|
+
parser._expect("THAN", "Expected 'than' after 'less'")
|
|
20
|
+
expr = parser._parse_expression()
|
|
21
|
+
return ast.FieldConstraint(kind="lt", expression=expr, line=tok.line, column=tok.column)
|
|
22
|
+
if parser._match("MATCH"):
|
|
23
|
+
parser._expect("PATTERN", "Expected 'pattern'")
|
|
24
|
+
pattern_tok = parser._expect("STRING", "Expected pattern string")
|
|
25
|
+
return ast.FieldConstraint(kind="pattern", pattern=pattern_tok.value, line=tok.line, column=tok.column)
|
|
26
|
+
if parser._match("HAVE"):
|
|
27
|
+
parser._expect("LENGTH", "Expected 'length'")
|
|
28
|
+
parser._expect("AT", "Expected 'at'")
|
|
29
|
+
if parser._match("LEAST"):
|
|
30
|
+
expr = parser._parse_expression()
|
|
31
|
+
return ast.FieldConstraint(kind="len_min", expression=expr, line=tok.line, column=tok.column)
|
|
32
|
+
if parser._match("MOST"):
|
|
33
|
+
expr = parser._parse_expression()
|
|
34
|
+
return ast.FieldConstraint(kind="len_max", expression=expr, line=tok.line, column=tok.column)
|
|
35
|
+
tok = parser._current()
|
|
36
|
+
raise Namel3ssError("Expected 'least' or 'most' after length", line=tok.line, column=tok.column)
|
|
37
|
+
raise Namel3ssError("Unknown constraint", line=tok.line, column=tok.column)
|
namel3ss/parser/core.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import List, Set
|
|
4
|
+
|
|
5
|
+
from namel3ss.ast import nodes as ast
|
|
6
|
+
from namel3ss.lexer.lexer import Lexer
|
|
7
|
+
from namel3ss.lexer.tokens import Token
|
|
8
|
+
from namel3ss.parser import tokens as token_ops
|
|
9
|
+
from namel3ss.parser.constraints import parse_field_constraint
|
|
10
|
+
from namel3ss.parser.expressions import (
|
|
11
|
+
parse_comparison,
|
|
12
|
+
parse_expression,
|
|
13
|
+
parse_not,
|
|
14
|
+
parse_or,
|
|
15
|
+
parse_primary,
|
|
16
|
+
parse_state_path,
|
|
17
|
+
)
|
|
18
|
+
from namel3ss.parser.expressions import parse_and as parse_and_expr
|
|
19
|
+
from namel3ss.parser.flow import parse_block, parse_flow, parse_statements
|
|
20
|
+
from namel3ss.parser.pages import parse_page, parse_page_item
|
|
21
|
+
from namel3ss.parser.program import parse_program
|
|
22
|
+
from namel3ss.parser.records import parse_record, parse_record_fields, type_from_token
|
|
23
|
+
from namel3ss.parser.statements import (
|
|
24
|
+
parse_find,
|
|
25
|
+
parse_for_each,
|
|
26
|
+
parse_if,
|
|
27
|
+
parse_let,
|
|
28
|
+
parse_match,
|
|
29
|
+
parse_repeat,
|
|
30
|
+
parse_return,
|
|
31
|
+
parse_save,
|
|
32
|
+
parse_set,
|
|
33
|
+
parse_statement,
|
|
34
|
+
parse_target,
|
|
35
|
+
parse_try,
|
|
36
|
+
validate_match_pattern,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Parser:
|
|
41
|
+
def __init__(self, tokens: List[Token]) -> None:
|
|
42
|
+
self.tokens = tokens
|
|
43
|
+
self.position = 0
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def parse(cls, source: str) -> ast.Program:
|
|
47
|
+
lexer = Lexer(source)
|
|
48
|
+
tokens = lexer.tokenize()
|
|
49
|
+
parser = cls(tokens)
|
|
50
|
+
program = parser._parse_program()
|
|
51
|
+
parser._expect("EOF")
|
|
52
|
+
return program
|
|
53
|
+
|
|
54
|
+
# Token helpers
|
|
55
|
+
def _current(self) -> Token:
|
|
56
|
+
return token_ops.current(self)
|
|
57
|
+
|
|
58
|
+
def _advance(self) -> Token:
|
|
59
|
+
return token_ops.advance(self)
|
|
60
|
+
|
|
61
|
+
def _match(self, *types: str) -> bool:
|
|
62
|
+
return token_ops.match(self, *types)
|
|
63
|
+
|
|
64
|
+
def _expect(self, token_type: str, message=None) -> Token:
|
|
65
|
+
return token_ops.expect(self, token_type, message)
|
|
66
|
+
|
|
67
|
+
# Program level
|
|
68
|
+
def _parse_program(self) -> ast.Program:
|
|
69
|
+
return parse_program(self)
|
|
70
|
+
|
|
71
|
+
# Flow and blocks
|
|
72
|
+
def _parse_flow(self) -> ast.Flow:
|
|
73
|
+
return parse_flow(self)
|
|
74
|
+
|
|
75
|
+
def _parse_statements(self, until: Set[str]) -> List[ast.Statement]:
|
|
76
|
+
return parse_statements(self, until)
|
|
77
|
+
|
|
78
|
+
def _parse_block(self) -> List[ast.Statement]:
|
|
79
|
+
return parse_block(self)
|
|
80
|
+
|
|
81
|
+
# Statements
|
|
82
|
+
def _parse_statement(self) -> ast.Statement:
|
|
83
|
+
return parse_statement(self)
|
|
84
|
+
|
|
85
|
+
def _parse_let(self) -> ast.Let:
|
|
86
|
+
return parse_let(self)
|
|
87
|
+
|
|
88
|
+
def _parse_set(self) -> ast.Set:
|
|
89
|
+
return parse_set(self)
|
|
90
|
+
|
|
91
|
+
def _parse_if(self) -> ast.If:
|
|
92
|
+
return parse_if(self)
|
|
93
|
+
|
|
94
|
+
def _parse_return(self) -> ast.Return:
|
|
95
|
+
return parse_return(self)
|
|
96
|
+
|
|
97
|
+
def _parse_repeat(self) -> ast.Repeat:
|
|
98
|
+
return parse_repeat(self)
|
|
99
|
+
|
|
100
|
+
def _parse_for_each(self) -> ast.ForEach:
|
|
101
|
+
return parse_for_each(self)
|
|
102
|
+
|
|
103
|
+
def _parse_match(self) -> ast.Match:
|
|
104
|
+
return parse_match(self)
|
|
105
|
+
|
|
106
|
+
def _parse_try(self) -> ast.TryCatch:
|
|
107
|
+
return parse_try(self)
|
|
108
|
+
|
|
109
|
+
def _parse_save(self) -> ast.Save:
|
|
110
|
+
return parse_save(self)
|
|
111
|
+
|
|
112
|
+
def _parse_find(self) -> ast.Find:
|
|
113
|
+
return parse_find(self)
|
|
114
|
+
|
|
115
|
+
def _parse_target(self) -> ast.Assignable:
|
|
116
|
+
return parse_target(self)
|
|
117
|
+
|
|
118
|
+
def _validate_match_pattern(self, pattern: ast.Expression) -> None:
|
|
119
|
+
return validate_match_pattern(self, pattern)
|
|
120
|
+
|
|
121
|
+
# Expressions
|
|
122
|
+
def _parse_expression(self) -> ast.Expression:
|
|
123
|
+
return parse_expression(self)
|
|
124
|
+
|
|
125
|
+
def _parse_or(self) -> ast.Expression:
|
|
126
|
+
return parse_or(self)
|
|
127
|
+
|
|
128
|
+
def _parse_and(self) -> ast.Expression:
|
|
129
|
+
return parse_and_expr(self)
|
|
130
|
+
|
|
131
|
+
def _parse_not(self) -> ast.Expression:
|
|
132
|
+
return parse_not(self)
|
|
133
|
+
|
|
134
|
+
def _parse_comparison(self) -> ast.Expression:
|
|
135
|
+
return parse_comparison(self)
|
|
136
|
+
|
|
137
|
+
def _parse_primary(self) -> ast.Expression:
|
|
138
|
+
return parse_primary(self)
|
|
139
|
+
|
|
140
|
+
def _parse_state_path(self) -> ast.StatePath:
|
|
141
|
+
return parse_state_path(self)
|
|
142
|
+
|
|
143
|
+
# Records and constraints
|
|
144
|
+
def _parse_record(self) -> ast.RecordDecl:
|
|
145
|
+
return parse_record(self)
|
|
146
|
+
|
|
147
|
+
def _parse_record_fields(self) -> List[ast.FieldDecl]:
|
|
148
|
+
return parse_record_fields(self)
|
|
149
|
+
|
|
150
|
+
def _parse_field_constraint(self) -> ast.FieldConstraint:
|
|
151
|
+
return parse_field_constraint(self)
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
def _type_from_token(tok: Token) -> str:
|
|
155
|
+
return type_from_token(tok)
|
|
156
|
+
|
|
157
|
+
# Pages
|
|
158
|
+
def _parse_page(self) -> ast.PageDecl:
|
|
159
|
+
return parse_page(self)
|
|
160
|
+
|
|
161
|
+
def _parse_page_item(self) -> ast.PageItem:
|
|
162
|
+
return parse_page_item(self)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def parse(source: str) -> ast.Program:
|
|
166
|
+
return Parser.parse(source)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from namel3ss.ast import nodes as ast
|
|
6
|
+
from namel3ss.errors.base import Namel3ssError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_expression(parser) -> ast.Expression:
|
|
10
|
+
return parse_or(parser)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_or(parser) -> ast.Expression:
|
|
14
|
+
expr = parse_and(parser)
|
|
15
|
+
while parser._match("OR"):
|
|
16
|
+
op_tok = parser.tokens[parser.position - 1]
|
|
17
|
+
right = parse_and(parser)
|
|
18
|
+
expr = ast.BinaryOp(op="or", left=expr, right=right, line=op_tok.line, column=op_tok.column)
|
|
19
|
+
return expr
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse_and(parser) -> ast.Expression:
|
|
23
|
+
expr = parse_not(parser)
|
|
24
|
+
while parser._match("AND"):
|
|
25
|
+
op_tok = parser.tokens[parser.position - 1]
|
|
26
|
+
right = parse_not(parser)
|
|
27
|
+
expr = ast.BinaryOp(op="and", left=expr, right=right, line=op_tok.line, column=op_tok.column)
|
|
28
|
+
return expr
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_not(parser) -> ast.Expression:
|
|
32
|
+
if parser._match("NOT"):
|
|
33
|
+
tok = parser.tokens[parser.position - 1]
|
|
34
|
+
operand = parse_not(parser)
|
|
35
|
+
return ast.UnaryOp(op="not", operand=operand, line=tok.line, column=tok.column)
|
|
36
|
+
return parse_comparison(parser)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def parse_comparison(parser) -> ast.Expression:
|
|
40
|
+
left = parse_primary(parser)
|
|
41
|
+
if not parser._match("IS"):
|
|
42
|
+
return left
|
|
43
|
+
is_tok = parser.tokens[parser.position - 1]
|
|
44
|
+
if parser._match("GREATER"):
|
|
45
|
+
parser._expect("THAN", "Expected 'than' after 'is greater'")
|
|
46
|
+
right = parse_primary(parser)
|
|
47
|
+
return ast.Comparison(kind="gt", left=left, right=right, line=is_tok.line, column=is_tok.column)
|
|
48
|
+
if parser._match("LESS"):
|
|
49
|
+
parser._expect("THAN", "Expected 'than' after 'is less'")
|
|
50
|
+
right = parse_primary(parser)
|
|
51
|
+
return ast.Comparison(kind="lt", left=left, right=right, line=is_tok.line, column=is_tok.column)
|
|
52
|
+
if parser._match("EQUAL"):
|
|
53
|
+
if parser._match("TO"):
|
|
54
|
+
pass
|
|
55
|
+
right = parse_primary(parser)
|
|
56
|
+
return ast.Comparison(kind="eq", left=left, right=right, line=is_tok.line, column=is_tok.column)
|
|
57
|
+
right = parse_primary(parser)
|
|
58
|
+
return ast.Comparison(kind="eq", left=left, right=right, line=is_tok.line, column=is_tok.column)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def parse_primary(parser) -> ast.Expression:
|
|
62
|
+
tok = parser._current()
|
|
63
|
+
if tok.type == "NUMBER":
|
|
64
|
+
parser._advance()
|
|
65
|
+
return ast.Literal(value=tok.value, line=tok.line, column=tok.column)
|
|
66
|
+
if tok.type == "STRING":
|
|
67
|
+
parser._advance()
|
|
68
|
+
return ast.Literal(value=tok.value, line=tok.line, column=tok.column)
|
|
69
|
+
if tok.type == "BOOLEAN":
|
|
70
|
+
parser._advance()
|
|
71
|
+
return ast.Literal(value=tok.value, line=tok.line, column=tok.column)
|
|
72
|
+
if tok.type in {"IDENT", "INPUT"}:
|
|
73
|
+
parser._advance()
|
|
74
|
+
attrs: List[str] = []
|
|
75
|
+
while parser._match("DOT"):
|
|
76
|
+
attr_tok = parser._expect("IDENT", "Expected identifier after '.'")
|
|
77
|
+
attrs.append(attr_tok.value)
|
|
78
|
+
if attrs:
|
|
79
|
+
return ast.AttrAccess(base=tok.value, attrs=attrs, line=tok.line, column=tok.column)
|
|
80
|
+
return ast.VarReference(name=tok.value, line=tok.line, column=tok.column)
|
|
81
|
+
if tok.type == "STATE":
|
|
82
|
+
return parse_state_path(parser)
|
|
83
|
+
if tok.type == "LPAREN":
|
|
84
|
+
parser._advance()
|
|
85
|
+
expr = parse_expression(parser)
|
|
86
|
+
parser._expect("RPAREN", "Expected ')'")
|
|
87
|
+
return expr
|
|
88
|
+
if tok.type == "ASK":
|
|
89
|
+
raise Namel3ssError(
|
|
90
|
+
'AI calls are statements. Use: ask ai "name" with input: <expr> as <target>.',
|
|
91
|
+
line=tok.line,
|
|
92
|
+
column=tok.column,
|
|
93
|
+
)
|
|
94
|
+
raise Namel3ssError("Unexpected expression", line=tok.line, column=tok.column)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def parse_state_path(parser) -> ast.StatePath:
|
|
98
|
+
state_tok = parser._expect("STATE", "Expected 'state'")
|
|
99
|
+
path: List[str] = []
|
|
100
|
+
while parser._match("DOT"):
|
|
101
|
+
ident_tok = parser._expect("IDENT", "Expected identifier after '.'")
|
|
102
|
+
path.append(ident_tok.value)
|
|
103
|
+
if not path:
|
|
104
|
+
raise Namel3ssError("Expected state path after 'state'", line=state_tok.line, column=state_tok.column)
|
|
105
|
+
return ast.StatePath(path=path, line=state_tok.line, column=state_tok.column)
|