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/parser/flow.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import List, Set
|
|
4
|
+
|
|
5
|
+
from namel3ss.ast import nodes as ast
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_flow(parser) -> ast.Flow:
|
|
9
|
+
flow_tok = parser._expect("FLOW", "Expected 'flow' declaration")
|
|
10
|
+
name_tok = parser._expect("STRING", "Expected flow name string")
|
|
11
|
+
parser._expect("COLON", "Expected ':' after flow name")
|
|
12
|
+
parser._expect("NEWLINE", "Expected newline after flow header")
|
|
13
|
+
parser._expect("INDENT", "Expected indented block for flow body")
|
|
14
|
+
body = parse_statements(parser, until={"DEDENT"})
|
|
15
|
+
parser._expect("DEDENT", "Expected block end")
|
|
16
|
+
while parser._match("NEWLINE"):
|
|
17
|
+
pass
|
|
18
|
+
return ast.Flow(name=name_tok.value, body=body, line=flow_tok.line, column=flow_tok.column)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_statements(parser, until: Set[str]) -> List[ast.Statement]:
|
|
22
|
+
statements: List[ast.Statement] = []
|
|
23
|
+
while parser._current().type not in until:
|
|
24
|
+
if parser._match("NEWLINE"):
|
|
25
|
+
continue
|
|
26
|
+
statements.append(parser._parse_statement())
|
|
27
|
+
return statements
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def parse_block(parser) -> List[ast.Statement]:
|
|
31
|
+
parser._expect("NEWLINE", "Expected newline before block")
|
|
32
|
+
parser._expect("INDENT", "Expected indented block")
|
|
33
|
+
stmts = parse_statements(parser, until={"DEDENT"})
|
|
34
|
+
parser._expect("DEDENT", "Expected end of block")
|
|
35
|
+
while parser._match("NEWLINE"):
|
|
36
|
+
pass
|
|
37
|
+
return stmts
|
namel3ss/parser/pages.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
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_page(parser) -> ast.PageDecl:
|
|
10
|
+
page_tok = parser._advance()
|
|
11
|
+
name_tok = parser._expect("STRING", "Expected page name string")
|
|
12
|
+
parser._expect("COLON", "Expected ':' after page name")
|
|
13
|
+
parser._expect("NEWLINE", "Expected newline after page header")
|
|
14
|
+
parser._expect("INDENT", "Expected indented page body")
|
|
15
|
+
items: List[ast.PageItem] = []
|
|
16
|
+
while parser._current().type != "DEDENT":
|
|
17
|
+
if parser._match("NEWLINE"):
|
|
18
|
+
continue
|
|
19
|
+
items.append(parse_page_item(parser))
|
|
20
|
+
parser._expect("DEDENT", "Expected end of page body")
|
|
21
|
+
return ast.PageDecl(name=name_tok.value, items=items, line=page_tok.line, column=page_tok.column)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_page_item(parser) -> ast.PageItem:
|
|
25
|
+
tok = parser._current()
|
|
26
|
+
if tok.type == "TITLE":
|
|
27
|
+
parser._advance()
|
|
28
|
+
parser._expect("IS", "Expected 'is' after 'title'")
|
|
29
|
+
value_tok = parser._expect("STRING", "Expected title string")
|
|
30
|
+
return ast.TitleItem(value=value_tok.value, line=tok.line, column=tok.column)
|
|
31
|
+
if tok.type == "TEXT":
|
|
32
|
+
parser._advance()
|
|
33
|
+
parser._expect("IS", "Expected 'is' after 'text'")
|
|
34
|
+
value_tok = parser._expect("STRING", "Expected text string")
|
|
35
|
+
return ast.TextItem(value=value_tok.value, line=tok.line, column=tok.column)
|
|
36
|
+
if tok.type == "FORM":
|
|
37
|
+
parser._advance()
|
|
38
|
+
parser._expect("IS", "Expected 'is' after 'form'")
|
|
39
|
+
value_tok = parser._expect("STRING", "Expected form record name")
|
|
40
|
+
return ast.FormItem(record_name=value_tok.value, line=tok.line, column=tok.column)
|
|
41
|
+
if tok.type == "TABLE":
|
|
42
|
+
parser._advance()
|
|
43
|
+
parser._expect("IS", "Expected 'is' after 'table'")
|
|
44
|
+
value_tok = parser._expect("STRING", "Expected table record name")
|
|
45
|
+
return ast.TableItem(record_name=value_tok.value, line=tok.line, column=tok.column)
|
|
46
|
+
if tok.type == "BUTTON":
|
|
47
|
+
parser._advance()
|
|
48
|
+
label_tok = parser._expect("STRING", "Expected button label string")
|
|
49
|
+
if parser._match("CALLS"):
|
|
50
|
+
raise Namel3ssError(
|
|
51
|
+
'Buttons must use a block. Use: button "Run": NEWLINE indent calls flow "demo"',
|
|
52
|
+
line=tok.line,
|
|
53
|
+
column=tok.column,
|
|
54
|
+
)
|
|
55
|
+
parser._expect("COLON", "Expected ':' after button label")
|
|
56
|
+
parser._expect("NEWLINE", "Expected newline after button header")
|
|
57
|
+
parser._expect("INDENT", "Expected indented button body")
|
|
58
|
+
flow_tok = None
|
|
59
|
+
while parser._current().type != "DEDENT":
|
|
60
|
+
if parser._match("NEWLINE"):
|
|
61
|
+
continue
|
|
62
|
+
parser._expect("CALLS", "Expected 'calls' in button action")
|
|
63
|
+
parser._expect("FLOW", "Expected 'flow' keyword in button action")
|
|
64
|
+
flow_tok = parser._expect("STRING", "Expected flow name string")
|
|
65
|
+
if parser._match("NEWLINE"):
|
|
66
|
+
continue
|
|
67
|
+
break
|
|
68
|
+
parser._expect("DEDENT", "Expected end of button body")
|
|
69
|
+
if flow_tok is None:
|
|
70
|
+
raise Namel3ssError("Button body must include 'calls flow \"<name>\"'", line=tok.line, column=tok.column)
|
|
71
|
+
return ast.ButtonItem(label=label_tok.value, flow_name=flow_tok.value, line=tok.line, column=tok.column)
|
|
72
|
+
raise Namel3ssError(
|
|
73
|
+
f"Pages are declarative; unexpected item '{tok.type.lower()}'",
|
|
74
|
+
line=tok.line,
|
|
75
|
+
column=tok.column,
|
|
76
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
from namel3ss.parser.agent import parse_agent_decl
|
|
8
|
+
from namel3ss.parser.ai import parse_ai_decl
|
|
9
|
+
from namel3ss.parser.flow import parse_flow
|
|
10
|
+
from namel3ss.parser.pages import parse_page
|
|
11
|
+
from namel3ss.parser.records import parse_record
|
|
12
|
+
from namel3ss.parser.tool import parse_tool
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_program(parser) -> ast.Program:
|
|
16
|
+
records: List[ast.RecordDecl] = []
|
|
17
|
+
flows: List[ast.Flow] = []
|
|
18
|
+
pages: List[ast.PageDecl] = []
|
|
19
|
+
ais: List[ast.AIDecl] = []
|
|
20
|
+
tools: List[ast.ToolDecl] = []
|
|
21
|
+
agents: List[ast.AgentDecl] = []
|
|
22
|
+
while parser._current().type != "EOF":
|
|
23
|
+
if parser._match("NEWLINE"):
|
|
24
|
+
continue
|
|
25
|
+
if parser._current().type == "TOOL":
|
|
26
|
+
tools.append(parse_tool(parser))
|
|
27
|
+
continue
|
|
28
|
+
if parser._current().type == "AGENT":
|
|
29
|
+
agents.append(parse_agent_decl(parser))
|
|
30
|
+
continue
|
|
31
|
+
if parser._current().type == "AI":
|
|
32
|
+
ais.append(parse_ai_decl(parser))
|
|
33
|
+
continue
|
|
34
|
+
if parser._current().type == "RECORD":
|
|
35
|
+
records.append(parse_record(parser))
|
|
36
|
+
continue
|
|
37
|
+
if parser._current().type == "FLOW":
|
|
38
|
+
flows.append(parse_flow(parser))
|
|
39
|
+
continue
|
|
40
|
+
if parser._current().type == "PAGE":
|
|
41
|
+
pages.append(parse_page(parser))
|
|
42
|
+
continue
|
|
43
|
+
tok = parser._current()
|
|
44
|
+
raise Namel3ssError("Unexpected top-level token", line=tok.line, column=tok.column)
|
|
45
|
+
return ast.Program(records=records, flows=flows, pages=pages, ais=ais, tools=tools, agents=agents, line=None, column=None)
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
from namel3ss.lexer.tokens import Token
|
|
8
|
+
from namel3ss.parser.constraints import parse_field_constraint
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def parse_record(parser) -> ast.RecordDecl:
|
|
12
|
+
rec_tok = parser._advance()
|
|
13
|
+
name_tok = parser._expect("STRING", "Expected record name string")
|
|
14
|
+
parser._expect("COLON", "Expected ':' after record name")
|
|
15
|
+
fields = parse_record_fields(parser)
|
|
16
|
+
return ast.RecordDecl(name=name_tok.value, fields=fields, line=rec_tok.line, column=rec_tok.column)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_record_fields(parser) -> List[ast.FieldDecl]:
|
|
20
|
+
parser._expect("NEWLINE", "Expected newline after record header")
|
|
21
|
+
parser._expect("INDENT", "Expected indented record body")
|
|
22
|
+
fields: List[ast.FieldDecl] = []
|
|
23
|
+
while parser._current().type != "DEDENT":
|
|
24
|
+
if parser._match("NEWLINE"):
|
|
25
|
+
continue
|
|
26
|
+
name_tok = parser._current()
|
|
27
|
+
if name_tok.type not in {"IDENT", "TITLE", "TEXT", "FORM", "TABLE", "BUTTON", "PAGE"}:
|
|
28
|
+
raise Namel3ssError("Expected field name", line=name_tok.line, column=name_tok.column)
|
|
29
|
+
parser._advance()
|
|
30
|
+
type_tok = parser._current()
|
|
31
|
+
if not type_tok.type.startswith("TYPE_"):
|
|
32
|
+
raise Namel3ssError("Expected field type", line=type_tok.line, column=type_tok.column)
|
|
33
|
+
parser._advance()
|
|
34
|
+
type_name = type_from_token(type_tok)
|
|
35
|
+
constraint = None
|
|
36
|
+
if parser._match("MUST"):
|
|
37
|
+
constraint = parse_field_constraint(parser)
|
|
38
|
+
fields.append(
|
|
39
|
+
ast.FieldDecl(
|
|
40
|
+
name=name_tok.value,
|
|
41
|
+
type_name=type_name,
|
|
42
|
+
constraint=constraint,
|
|
43
|
+
line=name_tok.line,
|
|
44
|
+
column=name_tok.column,
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
if parser._match("NEWLINE"):
|
|
48
|
+
continue
|
|
49
|
+
parser._expect("DEDENT", "Expected end of record body")
|
|
50
|
+
while parser._match("NEWLINE"):
|
|
51
|
+
pass
|
|
52
|
+
return fields
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def type_from_token(tok: Token) -> str:
|
|
56
|
+
if tok.type == "TYPE_STRING":
|
|
57
|
+
return "string"
|
|
58
|
+
if tok.type == "TYPE_INT":
|
|
59
|
+
return "int"
|
|
60
|
+
if tok.type == "TYPE_NUMBER":
|
|
61
|
+
return "number"
|
|
62
|
+
if tok.type == "TYPE_BOOLEAN":
|
|
63
|
+
return "boolean"
|
|
64
|
+
if tok.type == "TYPE_JSON":
|
|
65
|
+
return "json"
|
|
66
|
+
raise Namel3ssError("Invalid type", line=tok.line, column=tok.column)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from namel3ss.parser.statements.core import parse_statement, parse_target, validate_match_pattern
|
|
2
|
+
from namel3ss.parser.statements.control_flow import (
|
|
3
|
+
parse_for_each,
|
|
4
|
+
parse_if,
|
|
5
|
+
parse_match,
|
|
6
|
+
parse_repeat,
|
|
7
|
+
parse_return,
|
|
8
|
+
parse_try,
|
|
9
|
+
)
|
|
10
|
+
from namel3ss.parser.statements.data import parse_find, parse_save
|
|
11
|
+
from namel3ss.parser.statements.letset import parse_let, parse_set
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"parse_statement",
|
|
15
|
+
"parse_let",
|
|
16
|
+
"parse_set",
|
|
17
|
+
"parse_if",
|
|
18
|
+
"parse_return",
|
|
19
|
+
"parse_repeat",
|
|
20
|
+
"parse_for_each",
|
|
21
|
+
"parse_match",
|
|
22
|
+
"parse_try",
|
|
23
|
+
"parse_save",
|
|
24
|
+
"parse_find",
|
|
25
|
+
"parse_target",
|
|
26
|
+
"validate_match_pattern",
|
|
27
|
+
]
|
|
@@ -0,0 +1,116 @@
|
|
|
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_if(parser) -> ast.If:
|
|
10
|
+
if_tok = parser._advance()
|
|
11
|
+
condition = parser._parse_expression()
|
|
12
|
+
parser._expect("COLON", "Expected ':' after condition")
|
|
13
|
+
parser._expect("NEWLINE", "Expected newline after condition")
|
|
14
|
+
parser._expect("INDENT", "Expected indented block for if body")
|
|
15
|
+
then_body = parser._parse_statements(until={"DEDENT"})
|
|
16
|
+
parser._expect("DEDENT", "Expected block end")
|
|
17
|
+
else_body: List[ast.Statement] = []
|
|
18
|
+
while parser._match("NEWLINE"):
|
|
19
|
+
pass
|
|
20
|
+
if parser._match("ELSE"):
|
|
21
|
+
parser._expect("COLON", "Expected ':' after else")
|
|
22
|
+
parser._expect("NEWLINE", "Expected newline after else")
|
|
23
|
+
parser._expect("INDENT", "Expected indented block for else body")
|
|
24
|
+
else_body = parser._parse_statements(until={"DEDENT"})
|
|
25
|
+
parser._expect("DEDENT", "Expected block end")
|
|
26
|
+
while parser._match("NEWLINE"):
|
|
27
|
+
pass
|
|
28
|
+
return ast.If(
|
|
29
|
+
condition=condition,
|
|
30
|
+
then_body=then_body,
|
|
31
|
+
else_body=else_body,
|
|
32
|
+
line=if_tok.line,
|
|
33
|
+
column=if_tok.column,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def parse_return(parser) -> ast.Return:
|
|
38
|
+
ret_tok = parser._advance()
|
|
39
|
+
expr = parser._parse_expression()
|
|
40
|
+
return ast.Return(expression=expr, line=ret_tok.line, column=ret_tok.column)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def parse_repeat(parser) -> ast.Repeat:
|
|
44
|
+
rep_tok = parser._advance()
|
|
45
|
+
parser._expect("UP", "Expected 'up' in repeat statement")
|
|
46
|
+
parser._expect("TO", "Expected 'to' in repeat statement")
|
|
47
|
+
count_expr = parser._parse_expression()
|
|
48
|
+
parser._expect("TIMES", "Expected 'times' after repeat count")
|
|
49
|
+
parser._expect("COLON", "Expected ':' after repeat header")
|
|
50
|
+
body = parser._parse_block()
|
|
51
|
+
return ast.Repeat(count=count_expr, body=body, line=rep_tok.line, column=rep_tok.column)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_for_each(parser) -> ast.ForEach:
|
|
55
|
+
for_tok = parser._advance()
|
|
56
|
+
parser._expect("EACH", "Expected 'each' after 'for'")
|
|
57
|
+
name_tok = parser._expect("IDENT", "Expected loop variable name")
|
|
58
|
+
parser._expect("IN", "Expected 'in' in for-each loop")
|
|
59
|
+
iterable = parser._parse_expression()
|
|
60
|
+
parser._expect("COLON", "Expected ':' after for-each header")
|
|
61
|
+
body = parser._parse_block()
|
|
62
|
+
return ast.ForEach(name=name_tok.value, iterable=iterable, body=body, line=for_tok.line, column=for_tok.column)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def parse_match(parser) -> ast.Match:
|
|
66
|
+
match_tok = parser._advance()
|
|
67
|
+
expr = parser._parse_expression()
|
|
68
|
+
parser._expect("COLON", "Expected ':' after match expression")
|
|
69
|
+
parser._expect("NEWLINE", "Expected newline after match header")
|
|
70
|
+
parser._expect("INDENT", "Expected indented match body")
|
|
71
|
+
parser._expect("WITH", "Expected 'with' inside match")
|
|
72
|
+
parser._expect("COLON", "Expected ':' after 'with'")
|
|
73
|
+
parser._expect("NEWLINE", "Expected newline after 'with:'")
|
|
74
|
+
parser._expect("INDENT", "Expected indented match cases")
|
|
75
|
+
cases: List[ast.MatchCase] = []
|
|
76
|
+
otherwise_body: List[ast.Statement] | None = None
|
|
77
|
+
while parser._current().type not in {"DEDENT"}:
|
|
78
|
+
if parser._match("WHEN"):
|
|
79
|
+
pattern_expr = parser._parse_expression()
|
|
80
|
+
parser._validate_match_pattern(pattern_expr)
|
|
81
|
+
parser._expect("COLON", "Expected ':' after when pattern")
|
|
82
|
+
case_body = parser._parse_block()
|
|
83
|
+
if otherwise_body is not None:
|
|
84
|
+
raise Namel3ssError("Unreachable case after otherwise", line=pattern_expr.line, column=pattern_expr.column)
|
|
85
|
+
cases.append(ast.MatchCase(pattern=pattern_expr, body=case_body, line=pattern_expr.line, column=pattern_expr.column))
|
|
86
|
+
continue
|
|
87
|
+
if parser._match("OTHERWISE"):
|
|
88
|
+
if otherwise_body is not None:
|
|
89
|
+
tok = parser.tokens[parser.position - 1]
|
|
90
|
+
raise Namel3ssError("Duplicate otherwise in match", line=tok.line, column=tok.column)
|
|
91
|
+
parser._expect("COLON", "Expected ':' after otherwise")
|
|
92
|
+
otherwise_body = parser._parse_block()
|
|
93
|
+
continue
|
|
94
|
+
tok = parser._current()
|
|
95
|
+
raise Namel3ssError("Expected 'when' or 'otherwise' in match", line=tok.line, column=tok.column)
|
|
96
|
+
parser._expect("DEDENT", "Expected end of match cases")
|
|
97
|
+
parser._expect("DEDENT", "Expected end of match block")
|
|
98
|
+
while parser._match("NEWLINE"):
|
|
99
|
+
pass
|
|
100
|
+
if not cases and otherwise_body is None:
|
|
101
|
+
raise Namel3ssError("Match must have at least one case", line=match_tok.line, column=match_tok.column)
|
|
102
|
+
return ast.Match(expression=expr, cases=cases, otherwise=otherwise_body, line=match_tok.line, column=match_tok.column)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def parse_try(parser) -> ast.TryCatch:
|
|
106
|
+
try_tok = parser._advance()
|
|
107
|
+
parser._expect("COLON", "Expected ':' after try")
|
|
108
|
+
try_body = parser._parse_block()
|
|
109
|
+
if not parser._match("WITH"):
|
|
110
|
+
tok = parser._current()
|
|
111
|
+
raise Namel3ssError("Expected 'with' introducing catch", line=tok.line, column=tok.column)
|
|
112
|
+
parser._expect("CATCH", "Expected 'catch' after 'with'")
|
|
113
|
+
var_tok = parser._expect("IDENT", "Expected catch variable name")
|
|
114
|
+
parser._expect("COLON", "Expected ':' after catch clause")
|
|
115
|
+
catch_body = parser._parse_block()
|
|
116
|
+
return ast.TryCatch(try_body=try_body, catch_var=var_tok.value, catch_body=catch_body, line=try_tok.line, column=try_tok.column)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from namel3ss.ast import nodes as ast
|
|
4
|
+
from namel3ss.errors.base import Namel3ssError
|
|
5
|
+
from namel3ss.parser.agent import parse_run_agent_stmt, parse_run_agents_parallel
|
|
6
|
+
from namel3ss.parser.ai import parse_ask_stmt
|
|
7
|
+
from namel3ss.parser.statements.control_flow import (
|
|
8
|
+
parse_for_each,
|
|
9
|
+
parse_if,
|
|
10
|
+
parse_match,
|
|
11
|
+
parse_repeat,
|
|
12
|
+
parse_return,
|
|
13
|
+
parse_try,
|
|
14
|
+
)
|
|
15
|
+
from namel3ss.parser.statements.data import parse_find, parse_save
|
|
16
|
+
from namel3ss.parser.statements.letset import parse_let, parse_set
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_statement(parser) -> ast.Statement:
|
|
20
|
+
tok = parser._current()
|
|
21
|
+
if tok.type == "LET":
|
|
22
|
+
return parse_let(parser)
|
|
23
|
+
if tok.type == "SET":
|
|
24
|
+
return parse_set(parser)
|
|
25
|
+
if tok.type == "IF":
|
|
26
|
+
return parse_if(parser)
|
|
27
|
+
if tok.type == "RETURN":
|
|
28
|
+
return parse_return(parser)
|
|
29
|
+
if tok.type == "ASK":
|
|
30
|
+
return parse_ask_stmt(parser)
|
|
31
|
+
if tok.type == "RUN":
|
|
32
|
+
next_type = parser.tokens[parser.position + 1].type
|
|
33
|
+
if next_type == "AGENT":
|
|
34
|
+
return parse_run_agent_stmt(parser)
|
|
35
|
+
if next_type == "AGENTS":
|
|
36
|
+
return parse_run_agents_parallel(parser)
|
|
37
|
+
raise Namel3ssError("Expected 'agent' or 'agents' after run", line=tok.line, column=tok.column)
|
|
38
|
+
if tok.type == "REPEAT":
|
|
39
|
+
return parse_repeat(parser)
|
|
40
|
+
if tok.type == "FOR":
|
|
41
|
+
return parse_for_each(parser)
|
|
42
|
+
if tok.type == "MATCH":
|
|
43
|
+
return parse_match(parser)
|
|
44
|
+
if tok.type == "TRY":
|
|
45
|
+
return parse_try(parser)
|
|
46
|
+
if tok.type == "SAVE":
|
|
47
|
+
return parse_save(parser)
|
|
48
|
+
if tok.type == "FIND":
|
|
49
|
+
return parse_find(parser)
|
|
50
|
+
raise Namel3ssError(f"Unexpected token '{tok.type}' in statement", line=tok.line, column=tok.column)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def parse_target(parser) -> ast.Assignable:
|
|
54
|
+
tok = parser._current()
|
|
55
|
+
if tok.type == "STATE":
|
|
56
|
+
return parser._parse_state_path()
|
|
57
|
+
if tok.type == "IDENT":
|
|
58
|
+
name_tok = parser._advance()
|
|
59
|
+
return ast.VarReference(name=name_tok.value, line=name_tok.line, column=name_tok.column)
|
|
60
|
+
raise Namel3ssError("Expected assignment target", line=tok.line, column=tok.column)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def validate_match_pattern(parser, pattern: ast.Expression) -> None:
|
|
64
|
+
if isinstance(pattern, (ast.Literal, ast.VarReference, ast.StatePath)):
|
|
65
|
+
return
|
|
66
|
+
raise Namel3ssError("Match patterns must be literal or identifier", line=pattern.line, column=pattern.column)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from namel3ss.ast import nodes as ast
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def parse_save(parser) -> ast.Save:
|
|
7
|
+
tok = parser._advance()
|
|
8
|
+
name_tok = parser._expect("IDENT", "Expected record name after 'save'")
|
|
9
|
+
return ast.Save(record_name=name_tok.value, line=tok.line, column=tok.column)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_find(parser) -> ast.Find:
|
|
13
|
+
tok = parser._advance()
|
|
14
|
+
name_tok = parser._expect("IDENT", "Expected record name after 'find'")
|
|
15
|
+
parser._expect("WHERE", "Expected 'where' in find statement")
|
|
16
|
+
predicate = parser._parse_expression()
|
|
17
|
+
return ast.Find(record_name=name_tok.value, predicate=predicate, line=tok.line, column=tok.column)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from namel3ss.ast import nodes as ast
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def parse_let(parser) -> ast.Let:
|
|
7
|
+
let_tok = parser._advance()
|
|
8
|
+
name_tok = parser._expect("IDENT", "Expected identifier after 'let'")
|
|
9
|
+
parser._expect("IS", "Expected 'is' in declaration")
|
|
10
|
+
expr = parser._parse_expression()
|
|
11
|
+
constant = False
|
|
12
|
+
if parser._match("CONSTANT"):
|
|
13
|
+
constant = True
|
|
14
|
+
return ast.Let(name=name_tok.value, expression=expr, constant=constant, line=let_tok.line, column=let_tok.column)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_set(parser) -> ast.Set:
|
|
18
|
+
set_tok = parser._advance()
|
|
19
|
+
target = parser._parse_target()
|
|
20
|
+
parser._expect("IS", "Expected 'is' in assignment")
|
|
21
|
+
expr = parser._parse_expression()
|
|
22
|
+
return ast.Set(target=target, expression=expr, line=set_tok.line, column=set_tok.column)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from namel3ss.parser.statements import * # noqa: F401,F403
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from namel3ss.errors.base import Namel3ssError
|
|
6
|
+
from namel3ss.lexer.tokens import Token
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def current(parser) -> Token:
|
|
10
|
+
return parser.tokens[parser.position]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def advance(parser) -> Token:
|
|
14
|
+
tok = parser.tokens[parser.position]
|
|
15
|
+
parser.position += 1
|
|
16
|
+
return tok
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def match(parser, *types: str) -> bool:
|
|
20
|
+
if current(parser).type in types:
|
|
21
|
+
advance(parser)
|
|
22
|
+
return True
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def expect(parser, token_type: str, message: Optional[str] = None) -> Token:
|
|
27
|
+
tok = current(parser)
|
|
28
|
+
if tok.type != token_type:
|
|
29
|
+
raise Namel3ssError(
|
|
30
|
+
message or f"Expected {token_type}, got {tok.type}",
|
|
31
|
+
line=tok.line,
|
|
32
|
+
column=tok.column,
|
|
33
|
+
)
|
|
34
|
+
advance(parser)
|
|
35
|
+
return tok
|
namel3ss/parser/tool.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
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_tool(parser) -> ast.ToolDecl:
|
|
8
|
+
tool_tok = parser._advance()
|
|
9
|
+
name_tok = parser._expect("STRING", "Expected tool name string")
|
|
10
|
+
parser._expect("COLON", "Expected ':' after tool name")
|
|
11
|
+
parser._expect("NEWLINE", "Expected newline after tool header")
|
|
12
|
+
parser._expect("INDENT", "Expected indented tool body")
|
|
13
|
+
kind = None
|
|
14
|
+
while parser._current().type != "DEDENT":
|
|
15
|
+
if parser._match("NEWLINE"):
|
|
16
|
+
continue
|
|
17
|
+
key_tok = parser._current()
|
|
18
|
+
if key_tok.type == "KIND":
|
|
19
|
+
parser._advance()
|
|
20
|
+
parser._expect("IS", "Expected 'is' after kind")
|
|
21
|
+
kind_tok = parser._expect("STRING", "Expected kind string")
|
|
22
|
+
kind = kind_tok.value
|
|
23
|
+
else:
|
|
24
|
+
raise Namel3ssError("Unknown field in tool declaration", line=key_tok.line, column=key_tok.column)
|
|
25
|
+
parser._match("NEWLINE")
|
|
26
|
+
parser._expect("DEDENT", "Expected end of tool body")
|
|
27
|
+
if kind is None:
|
|
28
|
+
raise Namel3ssError("Tool declaration requires a kind", line=tool_tok.line, column=tool_tok.column)
|
|
29
|
+
return ast.ToolDecl(name=name_tok.value, kind=kind, line=tool_tok.line, column=tool_tok.column)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from urllib.error import HTTPError, URLError
|
|
5
|
+
from urllib.request import Request, urlopen
|
|
6
|
+
|
|
7
|
+
from namel3ss.errors.base import Namel3ssError
|
|
8
|
+
from namel3ss.runtime.ai.providers._shared.errors import map_http_error
|
|
9
|
+
from namel3ss.runtime.ai.providers._shared.parse import json_loads_or_error
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def post_json(*, url: str, headers: dict[str, str], payload: dict, timeout_seconds: int, provider_name: str) -> dict:
|
|
13
|
+
data = json.dumps(payload).encode("utf-8")
|
|
14
|
+
request = Request(url, data=data, headers=headers)
|
|
15
|
+
try:
|
|
16
|
+
with urlopen(request, timeout=timeout_seconds) as response:
|
|
17
|
+
body = response.read()
|
|
18
|
+
except (HTTPError, URLError, TimeoutError) as err:
|
|
19
|
+
raise map_http_error(provider_name, err) from err
|
|
20
|
+
except Exception as err: # pragma: no cover - unexpected transport errors
|
|
21
|
+
if isinstance(err, Namel3ssError):
|
|
22
|
+
raise
|
|
23
|
+
raise map_http_error(provider_name, err) from err
|
|
24
|
+
return json_loads_or_error(provider_name, body)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class AIResponse:
|
|
9
|
+
output: str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AIProvider:
|
|
13
|
+
def ask(
|
|
14
|
+
self,
|
|
15
|
+
*,
|
|
16
|
+
model: str,
|
|
17
|
+
system_prompt: Optional[str],
|
|
18
|
+
user_input: str,
|
|
19
|
+
tools: Optional[List[Dict[str, object]]] = None,
|
|
20
|
+
memory: Optional[Dict[str, object]] = None,
|
|
21
|
+
tool_results: Optional[List[Dict[str, object]]] = None,
|
|
22
|
+
) -> AIResponse:
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class AIToolCallResponse:
|
|
28
|
+
tool_name: str
|
|
29
|
+
args: Dict[str, object]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from namel3ss.runtime.ai.providers.mock import MockProvider
|
|
2
|
+
from namel3ss.runtime.ai.providers.ollama import OllamaProvider
|
|
3
|
+
from namel3ss.runtime.ai.providers.openai import OpenAIProvider
|
|
4
|
+
from namel3ss.runtime.ai.providers.anthropic import AnthropicProvider
|
|
5
|
+
from namel3ss.runtime.ai.providers.gemini import GeminiProvider
|
|
6
|
+
from namel3ss.runtime.ai.providers.mistral import MistralProvider
|
|
7
|
+
from namel3ss.runtime.ai.providers.registry import get_provider, is_supported_provider
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"MockProvider",
|
|
11
|
+
"OllamaProvider",
|
|
12
|
+
"OpenAIProvider",
|
|
13
|
+
"AnthropicProvider",
|
|
14
|
+
"GeminiProvider",
|
|
15
|
+
"MistralProvider",
|
|
16
|
+
"get_provider",
|
|
17
|
+
"is_supported_provider",
|
|
18
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from urllib.error import HTTPError, URLError
|
|
4
|
+
|
|
5
|
+
from namel3ss.errors.base import Namel3ssError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def require_env(provider_name: str, env_var: str, value: str | None) -> str:
|
|
9
|
+
if value is None or str(value).strip() == "":
|
|
10
|
+
short_var = env_var.replace("NAMEL3SS_", "")
|
|
11
|
+
raise Namel3ssError(f"Missing {short_var} (set it in .env or export it)")
|
|
12
|
+
return value
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def map_http_error(provider_name: str, err: HTTPError | URLError | TimeoutError | Exception) -> Namel3ssError:
|
|
16
|
+
if isinstance(err, HTTPError) and err.code in {401, 403}:
|
|
17
|
+
return Namel3ssError(f"Provider '{provider_name}' authentication failed")
|
|
18
|
+
if isinstance(err, (URLError, TimeoutError)):
|
|
19
|
+
return Namel3ssError(f"Provider '{provider_name}' unreachable")
|
|
20
|
+
return Namel3ssError(f"Provider '{provider_name}' returned an invalid response")
|