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,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Optional
|
|
4
|
+
|
|
5
|
+
from namel3ss.config.loader import load_config
|
|
6
|
+
from namel3ss.config.model import AppConfig
|
|
7
|
+
from namel3ss.ir import nodes as ir
|
|
8
|
+
from namel3ss.runtime.ai.mock_provider import MockProvider
|
|
9
|
+
from namel3ss.runtime.ai.provider import AIProvider
|
|
10
|
+
from namel3ss.runtime.executor.context import ExecutionContext
|
|
11
|
+
from namel3ss.runtime.executor.result import ExecutionResult
|
|
12
|
+
from namel3ss.runtime.executor.signals import _ReturnSignal
|
|
13
|
+
from namel3ss.runtime.executor.statements import execute_statement
|
|
14
|
+
from namel3ss.runtime.memory.manager import MemoryManager
|
|
15
|
+
from namel3ss.runtime.store.memory_store import MemoryStore
|
|
16
|
+
from namel3ss.schema.records import RecordSchema
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Executor:
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
flow: ir.Flow,
|
|
23
|
+
schemas: Optional[Dict[str, RecordSchema]] = None,
|
|
24
|
+
initial_state: Optional[Dict[str, object]] = None,
|
|
25
|
+
store: Optional[MemoryStore] = None,
|
|
26
|
+
input_data: Optional[Dict[str, object]] = None,
|
|
27
|
+
ai_provider: Optional[AIProvider] = None,
|
|
28
|
+
ai_profiles: Optional[Dict[str, ir.AIDecl]] = None,
|
|
29
|
+
memory_manager: Optional[MemoryManager] = None,
|
|
30
|
+
agents: Optional[Dict[str, ir.AgentDecl]] = None,
|
|
31
|
+
config: Optional[AppConfig] = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
resolved_config = config or load_config()
|
|
34
|
+
default_ai_provider = ai_provider or MockProvider()
|
|
35
|
+
provider_cache = {"mock": default_ai_provider}
|
|
36
|
+
self.ctx = ExecutionContext(
|
|
37
|
+
flow=flow,
|
|
38
|
+
schemas=schemas or {},
|
|
39
|
+
state=initial_state or {},
|
|
40
|
+
locals={"input": input_data or {}},
|
|
41
|
+
constants=set(),
|
|
42
|
+
last_value=None,
|
|
43
|
+
store=store or MemoryStore(),
|
|
44
|
+
ai_provider=default_ai_provider,
|
|
45
|
+
ai_profiles=ai_profiles or {},
|
|
46
|
+
agents=agents or {},
|
|
47
|
+
traces=[],
|
|
48
|
+
memory_manager=memory_manager or MemoryManager(),
|
|
49
|
+
agent_calls=0,
|
|
50
|
+
config=resolved_config,
|
|
51
|
+
provider_cache=provider_cache,
|
|
52
|
+
)
|
|
53
|
+
self.flow = self.ctx.flow
|
|
54
|
+
self.schemas = self.ctx.schemas
|
|
55
|
+
self.state = self.ctx.state
|
|
56
|
+
self.locals = self.ctx.locals
|
|
57
|
+
self.constants = self.ctx.constants
|
|
58
|
+
self.last_value = self.ctx.last_value
|
|
59
|
+
self.store = self.ctx.store
|
|
60
|
+
self.ai_provider = self.ctx.ai_provider
|
|
61
|
+
self.ai_profiles = self.ctx.ai_profiles
|
|
62
|
+
self.agents = self.ctx.agents
|
|
63
|
+
self.traces = self.ctx.traces
|
|
64
|
+
self.memory_manager = self.ctx.memory_manager
|
|
65
|
+
self.agent_calls = self.ctx.agent_calls
|
|
66
|
+
self.config = self.ctx.config
|
|
67
|
+
self.provider_cache = self.ctx.provider_cache
|
|
68
|
+
|
|
69
|
+
def run(self) -> ExecutionResult:
|
|
70
|
+
try:
|
|
71
|
+
for stmt in self.ctx.flow.body:
|
|
72
|
+
execute_statement(self.ctx, stmt)
|
|
73
|
+
except _ReturnSignal as signal:
|
|
74
|
+
self.ctx.last_value = signal.value
|
|
75
|
+
self.last_value = self.ctx.last_value
|
|
76
|
+
self.agent_calls = self.ctx.agent_calls
|
|
77
|
+
return ExecutionResult(state=self.ctx.state, last_value=self.ctx.last_value, traces=self.ctx.traces)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from namel3ss.errors.base import Namel3ssError
|
|
4
|
+
from namel3ss.ir import nodes as ir
|
|
5
|
+
from namel3ss.runtime.executor.context import ExecutionContext
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def evaluate_expression(ctx: ExecutionContext, expr: ir.Expression) -> object:
|
|
9
|
+
if isinstance(expr, ir.Literal):
|
|
10
|
+
return expr.value
|
|
11
|
+
if isinstance(expr, ir.VarReference):
|
|
12
|
+
if expr.name not in ctx.locals:
|
|
13
|
+
raise Namel3ssError(
|
|
14
|
+
f"Unknown variable '{expr.name}'",
|
|
15
|
+
line=expr.line,
|
|
16
|
+
column=expr.column,
|
|
17
|
+
)
|
|
18
|
+
return ctx.locals[expr.name]
|
|
19
|
+
if isinstance(expr, ir.AttrAccess):
|
|
20
|
+
if expr.base not in ctx.locals:
|
|
21
|
+
raise Namel3ssError(
|
|
22
|
+
f"Unknown variable '{expr.base}'",
|
|
23
|
+
line=expr.line,
|
|
24
|
+
column=expr.column,
|
|
25
|
+
)
|
|
26
|
+
value = ctx.locals[expr.base]
|
|
27
|
+
for attr in expr.attrs:
|
|
28
|
+
if isinstance(value, dict):
|
|
29
|
+
if attr not in value:
|
|
30
|
+
raise Namel3ssError(
|
|
31
|
+
f"Missing attribute '{attr}'",
|
|
32
|
+
line=expr.line,
|
|
33
|
+
column=expr.column,
|
|
34
|
+
)
|
|
35
|
+
value = value[attr]
|
|
36
|
+
continue
|
|
37
|
+
if not hasattr(value, attr):
|
|
38
|
+
raise Namel3ssError(
|
|
39
|
+
f"Missing attribute '{attr}'",
|
|
40
|
+
line=expr.line,
|
|
41
|
+
column=expr.column,
|
|
42
|
+
)
|
|
43
|
+
value = getattr(value, attr)
|
|
44
|
+
return value
|
|
45
|
+
if isinstance(expr, ir.StatePath):
|
|
46
|
+
return resolve_state_path(ctx, expr)
|
|
47
|
+
if isinstance(expr, ir.UnaryOp):
|
|
48
|
+
operand = evaluate_expression(ctx, expr.operand)
|
|
49
|
+
if expr.op == "not":
|
|
50
|
+
if not isinstance(operand, bool):
|
|
51
|
+
raise Namel3ssError("Operand to 'not' must be boolean", line=expr.line, column=expr.column)
|
|
52
|
+
return not operand
|
|
53
|
+
raise Namel3ssError(f"Unsupported unary op '{expr.op}'", line=expr.line, column=expr.column)
|
|
54
|
+
if isinstance(expr, ir.BinaryOp):
|
|
55
|
+
if expr.op == "and":
|
|
56
|
+
left = evaluate_expression(ctx, expr.left)
|
|
57
|
+
if not isinstance(left, bool):
|
|
58
|
+
raise Namel3ssError("Left operand of 'and' must be boolean", line=expr.line, column=expr.column)
|
|
59
|
+
if not left:
|
|
60
|
+
return False
|
|
61
|
+
right = evaluate_expression(ctx, expr.right)
|
|
62
|
+
if not isinstance(right, bool):
|
|
63
|
+
raise Namel3ssError("Right operand of 'and' must be boolean", line=expr.line, column=expr.column)
|
|
64
|
+
return left and right
|
|
65
|
+
if expr.op == "or":
|
|
66
|
+
left = evaluate_expression(ctx, expr.left)
|
|
67
|
+
if not isinstance(left, bool):
|
|
68
|
+
raise Namel3ssError("Left operand of 'or' must be boolean", line=expr.line, column=expr.column)
|
|
69
|
+
if left:
|
|
70
|
+
return True
|
|
71
|
+
right = evaluate_expression(ctx, expr.right)
|
|
72
|
+
if not isinstance(right, bool):
|
|
73
|
+
raise Namel3ssError("Right operand of 'or' must be boolean", line=expr.line, column=expr.column)
|
|
74
|
+
return bool(right)
|
|
75
|
+
raise Namel3ssError(f"Unsupported binary op '{expr.op}'", line=expr.line, column=expr.column)
|
|
76
|
+
if isinstance(expr, ir.Comparison):
|
|
77
|
+
left = evaluate_expression(ctx, expr.left)
|
|
78
|
+
right = evaluate_expression(ctx, expr.right)
|
|
79
|
+
if expr.kind in {"gt", "lt"}:
|
|
80
|
+
if not isinstance(left, (int, float)) or not isinstance(right, (int, float)):
|
|
81
|
+
raise Namel3ssError(
|
|
82
|
+
"Greater/less comparisons require numbers",
|
|
83
|
+
line=expr.line,
|
|
84
|
+
column=expr.column,
|
|
85
|
+
)
|
|
86
|
+
return left > right if expr.kind == "gt" else left < right
|
|
87
|
+
if expr.kind == "eq":
|
|
88
|
+
return left == right
|
|
89
|
+
raise Namel3ssError(f"Unsupported comparison '{expr.kind}'", line=expr.line, column=expr.column)
|
|
90
|
+
|
|
91
|
+
raise Namel3ssError(f"Unsupported expression type: {type(expr)}", line=expr.line, column=expr.column)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def resolve_state_path(ctx: ExecutionContext, expr: ir.StatePath) -> object:
|
|
95
|
+
cursor: object = ctx.state
|
|
96
|
+
for segment in expr.path:
|
|
97
|
+
if not isinstance(cursor, dict):
|
|
98
|
+
raise Namel3ssError(
|
|
99
|
+
f"State path '{'.'.join(expr.path)}' is not a mapping",
|
|
100
|
+
line=expr.line,
|
|
101
|
+
column=expr.column,
|
|
102
|
+
)
|
|
103
|
+
if segment not in cursor:
|
|
104
|
+
raise Namel3ssError(
|
|
105
|
+
f"Unknown state path '{'.'.join(expr.path)}'",
|
|
106
|
+
line=expr.line,
|
|
107
|
+
column=expr.column,
|
|
108
|
+
)
|
|
109
|
+
cursor = cursor[segment]
|
|
110
|
+
return cursor
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from namel3ss.errors.base import Namel3ssError
|
|
4
|
+
from namel3ss.ir import nodes as ir
|
|
5
|
+
from namel3ss.runtime.executor.context import ExecutionContext
|
|
6
|
+
from namel3ss.runtime.executor.expr_eval import evaluate_expression
|
|
7
|
+
from namel3ss.runtime.records.service import save_record_or_raise
|
|
8
|
+
from namel3ss.schema.records import RecordSchema
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def handle_save(ctx: ExecutionContext, stmt: ir.Save) -> None:
|
|
12
|
+
state_key = stmt.record_name.lower()
|
|
13
|
+
data_obj = ctx.state.get(state_key)
|
|
14
|
+
if not isinstance(data_obj, dict):
|
|
15
|
+
raise Namel3ssError(
|
|
16
|
+
f"Expected state.{state_key} to be a record dictionary",
|
|
17
|
+
line=stmt.line,
|
|
18
|
+
column=stmt.column,
|
|
19
|
+
)
|
|
20
|
+
validated = dict(data_obj)
|
|
21
|
+
saved = save_record_or_raise(
|
|
22
|
+
stmt.record_name,
|
|
23
|
+
validated,
|
|
24
|
+
ctx.schemas,
|
|
25
|
+
ctx.state,
|
|
26
|
+
ctx.store,
|
|
27
|
+
line=stmt.line,
|
|
28
|
+
column=stmt.column,
|
|
29
|
+
)
|
|
30
|
+
ctx.last_value = saved
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def handle_find(ctx: ExecutionContext, stmt: ir.Find) -> None:
|
|
34
|
+
schema = get_schema(ctx, stmt.record_name, stmt)
|
|
35
|
+
|
|
36
|
+
def predicate(record: dict) -> bool:
|
|
37
|
+
backup_locals = ctx.locals.copy()
|
|
38
|
+
try:
|
|
39
|
+
ctx.locals.update(record)
|
|
40
|
+
result = evaluate_expression(ctx, stmt.predicate)
|
|
41
|
+
if not isinstance(result, bool):
|
|
42
|
+
raise Namel3ssError(
|
|
43
|
+
"Find predicate must evaluate to boolean",
|
|
44
|
+
line=stmt.line,
|
|
45
|
+
column=stmt.column,
|
|
46
|
+
)
|
|
47
|
+
return result
|
|
48
|
+
finally:
|
|
49
|
+
ctx.locals = backup_locals
|
|
50
|
+
|
|
51
|
+
results = ctx.store.find(schema, predicate)
|
|
52
|
+
result_name = f"{stmt.record_name.lower()}_results"
|
|
53
|
+
ctx.locals[result_name] = results
|
|
54
|
+
ctx.last_value = results
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_schema(ctx: ExecutionContext, record_name: str, stmt: ir.Statement) -> RecordSchema:
|
|
58
|
+
if record_name not in ctx.schemas:
|
|
59
|
+
raise Namel3ssError(
|
|
60
|
+
f"Unknown record '{record_name}'",
|
|
61
|
+
line=stmt.line,
|
|
62
|
+
column=stmt.column,
|
|
63
|
+
)
|
|
64
|
+
return ctx.schemas[record_name]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Dict, Optional
|
|
5
|
+
|
|
6
|
+
from namel3ss.runtime.ai.trace import AITrace
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class ExecutionResult:
|
|
11
|
+
state: Dict[str, object]
|
|
12
|
+
last_value: Optional[object]
|
|
13
|
+
traces: list[AITrace]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from namel3ss.errors.base import Namel3ssError
|
|
4
|
+
from namel3ss.ir import nodes as ir
|
|
5
|
+
from namel3ss.runtime.executor.ai_runner import execute_ask_ai
|
|
6
|
+
from namel3ss.runtime.executor.agents import execute_run_agent, execute_run_agents_parallel
|
|
7
|
+
from namel3ss.runtime.executor.assign import assign
|
|
8
|
+
from namel3ss.runtime.executor.context import ExecutionContext
|
|
9
|
+
from namel3ss.runtime.executor.expr_eval import evaluate_expression
|
|
10
|
+
from namel3ss.runtime.executor.records_ops import handle_find, handle_save
|
|
11
|
+
from namel3ss.runtime.executor.signals import _ReturnSignal
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def execute_statement(ctx: ExecutionContext, stmt: ir.Statement) -> None:
|
|
15
|
+
if isinstance(stmt, ir.Let):
|
|
16
|
+
value = evaluate_expression(ctx, stmt.expression)
|
|
17
|
+
ctx.locals[stmt.name] = value
|
|
18
|
+
if stmt.constant:
|
|
19
|
+
ctx.constants.add(stmt.name)
|
|
20
|
+
ctx.last_value = value
|
|
21
|
+
return
|
|
22
|
+
if isinstance(stmt, ir.Set):
|
|
23
|
+
value = evaluate_expression(ctx, stmt.expression)
|
|
24
|
+
assign(ctx, stmt.target, value, stmt)
|
|
25
|
+
ctx.last_value = value
|
|
26
|
+
return
|
|
27
|
+
if isinstance(stmt, ir.If):
|
|
28
|
+
condition_value = evaluate_expression(ctx, stmt.condition)
|
|
29
|
+
if not isinstance(condition_value, bool):
|
|
30
|
+
raise Namel3ssError(
|
|
31
|
+
"Condition must evaluate to a boolean",
|
|
32
|
+
line=stmt.line,
|
|
33
|
+
column=stmt.column,
|
|
34
|
+
)
|
|
35
|
+
branch = stmt.then_body if condition_value else stmt.else_body
|
|
36
|
+
for child in branch:
|
|
37
|
+
execute_statement(ctx, child)
|
|
38
|
+
return
|
|
39
|
+
if isinstance(stmt, ir.Return):
|
|
40
|
+
value = evaluate_expression(ctx, stmt.expression)
|
|
41
|
+
raise _ReturnSignal(value)
|
|
42
|
+
if isinstance(stmt, ir.Repeat):
|
|
43
|
+
count_value = evaluate_expression(ctx, stmt.count)
|
|
44
|
+
if not isinstance(count_value, int):
|
|
45
|
+
raise Namel3ssError("Repeat count must be an integer", line=stmt.line, column=stmt.column)
|
|
46
|
+
if count_value < 0:
|
|
47
|
+
raise Namel3ssError("Repeat count cannot be negative", line=stmt.line, column=stmt.column)
|
|
48
|
+
for _ in range(count_value):
|
|
49
|
+
for child in stmt.body:
|
|
50
|
+
execute_statement(ctx, child)
|
|
51
|
+
return
|
|
52
|
+
if isinstance(stmt, ir.ForEach):
|
|
53
|
+
iterable_value = evaluate_expression(ctx, stmt.iterable)
|
|
54
|
+
if not isinstance(iterable_value, list):
|
|
55
|
+
raise Namel3ssError("For-each expects a list", line=stmt.line, column=stmt.column)
|
|
56
|
+
for item in iterable_value:
|
|
57
|
+
ctx.locals[stmt.name] = item
|
|
58
|
+
for child in stmt.body:
|
|
59
|
+
execute_statement(ctx, child)
|
|
60
|
+
return
|
|
61
|
+
if isinstance(stmt, ir.Match):
|
|
62
|
+
subject = evaluate_expression(ctx, stmt.expression)
|
|
63
|
+
matched = False
|
|
64
|
+
for case in stmt.cases:
|
|
65
|
+
pattern_value = evaluate_expression(ctx, case.pattern)
|
|
66
|
+
if subject == pattern_value:
|
|
67
|
+
matched = True
|
|
68
|
+
for child in case.body:
|
|
69
|
+
execute_statement(ctx, child)
|
|
70
|
+
break
|
|
71
|
+
if not matched and stmt.otherwise is not None:
|
|
72
|
+
for child in stmt.otherwise:
|
|
73
|
+
execute_statement(ctx, child)
|
|
74
|
+
return
|
|
75
|
+
if isinstance(stmt, ir.TryCatch):
|
|
76
|
+
try:
|
|
77
|
+
for child in stmt.try_body:
|
|
78
|
+
execute_statement(ctx, child)
|
|
79
|
+
except Namel3ssError as err:
|
|
80
|
+
ctx.locals[stmt.catch_var] = err
|
|
81
|
+
for child in stmt.catch_body:
|
|
82
|
+
execute_statement(ctx, child)
|
|
83
|
+
return
|
|
84
|
+
if isinstance(stmt, ir.AskAIStmt):
|
|
85
|
+
execute_ask_ai(ctx, stmt)
|
|
86
|
+
return
|
|
87
|
+
if isinstance(stmt, ir.RunAgentStmt):
|
|
88
|
+
execute_run_agent(ctx, stmt)
|
|
89
|
+
return
|
|
90
|
+
if isinstance(stmt, ir.RunAgentsParallelStmt):
|
|
91
|
+
execute_run_agents_parallel(ctx, stmt)
|
|
92
|
+
return
|
|
93
|
+
if isinstance(stmt, ir.Save):
|
|
94
|
+
handle_save(ctx, stmt)
|
|
95
|
+
return
|
|
96
|
+
if isinstance(stmt, ir.Find):
|
|
97
|
+
handle_find(ctx, stmt)
|
|
98
|
+
return
|
|
99
|
+
raise Namel3ssError(f"Unsupported statement type: {type(stmt)}", line=stmt.line, column=stmt.column)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
from namel3ss.ir import nodes as ir
|
|
6
|
+
from namel3ss.runtime.memory.profile import ProfileMemory
|
|
7
|
+
from namel3ss.runtime.memory.semantic import SemanticMemory
|
|
8
|
+
from namel3ss.runtime.memory.short_term import ShortTermMemory
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MemoryManager:
|
|
12
|
+
def __init__(self) -> None:
|
|
13
|
+
self.short_term = ShortTermMemory()
|
|
14
|
+
self.profile = ProfileMemory()
|
|
15
|
+
self.semantic = SemanticMemory()
|
|
16
|
+
|
|
17
|
+
def _session(self, state: Dict[str, object]) -> str:
|
|
18
|
+
if isinstance(state.get("user"), dict) and "id" in state["user"]:
|
|
19
|
+
return str(state["user"]["id"])
|
|
20
|
+
return "anonymous"
|
|
21
|
+
|
|
22
|
+
def recall_context(self, ai: ir.AIDecl, user_input: str, state: Dict[str, object]) -> dict:
|
|
23
|
+
session = self._session(state)
|
|
24
|
+
memory = ai.memory
|
|
25
|
+
context = {"short_term": [], "semantic": [], "profile": []}
|
|
26
|
+
if memory.short_term > 0:
|
|
27
|
+
context["short_term"] = self.short_term.recall(session, memory.short_term)
|
|
28
|
+
if memory.semantic:
|
|
29
|
+
context["semantic"] = self.semantic.recall(session, user_input, top_k=3)
|
|
30
|
+
if memory.profile:
|
|
31
|
+
context["profile"] = self.profile.recall(session)
|
|
32
|
+
return context
|
|
33
|
+
|
|
34
|
+
def record_interaction(
|
|
35
|
+
self,
|
|
36
|
+
ai: ir.AIDecl,
|
|
37
|
+
state: Dict[str, object],
|
|
38
|
+
user_input: str,
|
|
39
|
+
ai_output: str,
|
|
40
|
+
tool_events: List[dict],
|
|
41
|
+
) -> None:
|
|
42
|
+
session = self._session(state)
|
|
43
|
+
message = {"role": "user", "content": user_input}
|
|
44
|
+
self.short_term.record(session, message)
|
|
45
|
+
ai_message = {"role": "ai", "content": ai_output}
|
|
46
|
+
self.short_term.record(session, ai_message)
|
|
47
|
+
if ai.memory.semantic:
|
|
48
|
+
snippet = f"user:{user_input} ai:{ai_output}"
|
|
49
|
+
if tool_events:
|
|
50
|
+
snippet += f" tools:{tool_events}"
|
|
51
|
+
self.semantic.record(session, snippet)
|
|
52
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ProfileMemory:
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
self._facts: Dict[str, Dict[str, str]] = {}
|
|
9
|
+
|
|
10
|
+
def set_fact(self, session: str, key: str, value: str) -> None:
|
|
11
|
+
facts = self._facts.setdefault(session, {})
|
|
12
|
+
facts[key] = value
|
|
13
|
+
|
|
14
|
+
def recall(self, session: str, limit: int = 20) -> List[dict]:
|
|
15
|
+
facts = self._facts.get(session, {})
|
|
16
|
+
items = list(facts.items())[:limit]
|
|
17
|
+
return [{"key": k, "value": v} for k, v in items]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SemanticMemory:
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
self._snippets: Dict[str, List[str]] = {}
|
|
9
|
+
|
|
10
|
+
def record(self, session: str, snippet: str) -> None:
|
|
11
|
+
self._snippets.setdefault(session, []).append(snippet)
|
|
12
|
+
|
|
13
|
+
def recall(self, session: str, query: str, top_k: int = 3) -> List[dict]:
|
|
14
|
+
snippets = self._snippets.get(session, [])
|
|
15
|
+
matches = []
|
|
16
|
+
for text in snippets:
|
|
17
|
+
score = 1 if query and query.lower() in text.lower() else 0
|
|
18
|
+
matches.append({"text": text, "score": score})
|
|
19
|
+
matches.sort(key=lambda x: x["score"], reverse=True)
|
|
20
|
+
return matches[:top_k]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ShortTermMemory:
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
self._messages: Dict[str, List[dict]] = {}
|
|
9
|
+
|
|
10
|
+
def record(self, session: str, message: dict) -> None:
|
|
11
|
+
messages = self._messages.setdefault(session, [])
|
|
12
|
+
messages.append(message)
|
|
13
|
+
|
|
14
|
+
def recall(self, session: str, limit: int) -> List[dict]:
|
|
15
|
+
messages = self._messages.get(session, [])
|
|
16
|
+
if limit <= 0:
|
|
17
|
+
return []
|
|
18
|
+
return messages[-limit:]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
from namel3ss.errors.base import Namel3ssError
|
|
6
|
+
from namel3ss.ir import nodes as ir
|
|
7
|
+
from namel3ss.runtime.store.memory_store import MemoryStore
|
|
8
|
+
from namel3ss.runtime.validators.constraints import collect_validation_errors, validate_record_instance
|
|
9
|
+
from namel3ss.schema.records import RecordSchema
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def save_record_or_raise(
|
|
13
|
+
record_name: str,
|
|
14
|
+
values: Dict[str, object],
|
|
15
|
+
schemas: Dict[str, RecordSchema],
|
|
16
|
+
state: Dict[str, object],
|
|
17
|
+
store: MemoryStore,
|
|
18
|
+
line: int | None = None,
|
|
19
|
+
column: int | None = None,
|
|
20
|
+
) -> dict:
|
|
21
|
+
saved, errors = save_record_with_errors(record_name, values, schemas, state, store)
|
|
22
|
+
if errors:
|
|
23
|
+
first = errors[0]
|
|
24
|
+
raise Namel3ssError(first["message"], line=line, column=column)
|
|
25
|
+
return saved
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def save_record_with_errors(
|
|
29
|
+
record_name: str,
|
|
30
|
+
values: Dict[str, object],
|
|
31
|
+
schemas: Dict[str, RecordSchema],
|
|
32
|
+
state: Dict[str, object],
|
|
33
|
+
store: MemoryStore,
|
|
34
|
+
) -> Tuple[Optional[dict], List[Dict[str, str]]]:
|
|
35
|
+
schema = _get_schema(record_name, schemas)
|
|
36
|
+
type_errors = _type_errors(schema, values)
|
|
37
|
+
if type_errors:
|
|
38
|
+
return None, type_errors
|
|
39
|
+
|
|
40
|
+
constraint_errors = collect_validation_errors(schema, values, _literal_eval)
|
|
41
|
+
if constraint_errors:
|
|
42
|
+
return None, constraint_errors
|
|
43
|
+
|
|
44
|
+
conflict_field = store.check_unique(schema, values)
|
|
45
|
+
if conflict_field:
|
|
46
|
+
return None, [
|
|
47
|
+
{
|
|
48
|
+
"field": conflict_field,
|
|
49
|
+
"code": "unique",
|
|
50
|
+
"message": f"Field '{conflict_field}' in record '{record_name}' must be unique",
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
try:
|
|
54
|
+
saved = store.save(schema, values)
|
|
55
|
+
return saved, []
|
|
56
|
+
except Namel3ssError as exc:
|
|
57
|
+
# Fallback for any residual unique enforcement
|
|
58
|
+
return None, [
|
|
59
|
+
{
|
|
60
|
+
"field": conflict_field or "",
|
|
61
|
+
"code": "unique",
|
|
62
|
+
"message": str(exc),
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _type_errors(schema: RecordSchema, data: Dict[str, object]) -> List[Dict[str, str]]:
|
|
68
|
+
errors: List[Dict[str, str]] = []
|
|
69
|
+
for field in schema.fields:
|
|
70
|
+
value = data.get(field.name)
|
|
71
|
+
if value is None:
|
|
72
|
+
continue
|
|
73
|
+
expected = field.type_name
|
|
74
|
+
if expected == "string" and not isinstance(value, str):
|
|
75
|
+
errors.append(_type_error(field.name, schema.name, "string"))
|
|
76
|
+
elif expected == "int" and not isinstance(value, int):
|
|
77
|
+
errors.append(_type_error(field.name, schema.name, "int"))
|
|
78
|
+
elif expected == "number" and not isinstance(value, (int, float)):
|
|
79
|
+
errors.append(_type_error(field.name, schema.name, "number"))
|
|
80
|
+
elif expected == "boolean" and not isinstance(value, bool):
|
|
81
|
+
errors.append(_type_error(field.name, schema.name, "boolean"))
|
|
82
|
+
return errors
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _type_error(field: str, record: str, expected: str) -> Dict[str, str]:
|
|
86
|
+
return {
|
|
87
|
+
"field": field,
|
|
88
|
+
"code": "type",
|
|
89
|
+
"message": f"Field '{field}' in record '{record}' must be a {expected}",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _get_schema(name: str, schemas: Dict[str, RecordSchema]) -> RecordSchema:
|
|
94
|
+
if name not in schemas:
|
|
95
|
+
raise Namel3ssError(f"Unknown record '{name}'")
|
|
96
|
+
return schemas[name]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _literal_eval(expr: ir.Expression | None) -> object:
|
|
100
|
+
if expr is None:
|
|
101
|
+
return None
|
|
102
|
+
if isinstance(expr, ir.Literal):
|
|
103
|
+
return expr.value
|
|
104
|
+
raise Namel3ssError("Only literal expressions supported in schema constraints for forms")
|
|
105
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Callable
|
|
4
|
+
|
|
5
|
+
from namel3ss.errors.base import Namel3ssError
|
|
6
|
+
from namel3ss.schema.records import RecordSchema
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MemoryStore:
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
self._data: Dict[str, List[dict]] = {}
|
|
12
|
+
self._unique_indexes: Dict[str, Dict[str, Dict[object, dict]]] = {}
|
|
13
|
+
self._counters: Dict[str, int] = {}
|
|
14
|
+
|
|
15
|
+
def save(self, schema: RecordSchema, record: dict) -> dict:
|
|
16
|
+
rec_name = schema.name
|
|
17
|
+
if rec_name not in self._data:
|
|
18
|
+
self._data[rec_name] = []
|
|
19
|
+
self._unique_indexes[rec_name] = {}
|
|
20
|
+
self._counters[rec_name] = 1
|
|
21
|
+
|
|
22
|
+
# Handle auto id
|
|
23
|
+
if "id" in schema.field_map:
|
|
24
|
+
record.setdefault("id", self._counters[rec_name])
|
|
25
|
+
else:
|
|
26
|
+
record.setdefault("_id", self._counters[rec_name])
|
|
27
|
+
self._counters[rec_name] += 1
|
|
28
|
+
|
|
29
|
+
conflict_field = self.check_unique(schema, record)
|
|
30
|
+
if conflict_field:
|
|
31
|
+
raise Namel3ssError(f"Record '{rec_name}' violates unique constraint on '{conflict_field}'")
|
|
32
|
+
for field in schema.unique_fields:
|
|
33
|
+
value = record.get(field)
|
|
34
|
+
if value is None:
|
|
35
|
+
continue
|
|
36
|
+
idx = self._unique_indexes[rec_name].setdefault(field, {})
|
|
37
|
+
idx[value] = record
|
|
38
|
+
|
|
39
|
+
self._data[rec_name].append(record)
|
|
40
|
+
return record
|
|
41
|
+
|
|
42
|
+
def find(self, schema: RecordSchema, predicate: Callable[[dict], bool]) -> List[dict]:
|
|
43
|
+
records = self._data.get(schema.name, [])
|
|
44
|
+
return [rec for rec in records if predicate(rec)]
|
|
45
|
+
|
|
46
|
+
def check_unique(self, schema: RecordSchema, record: dict) -> str | None:
|
|
47
|
+
rec_name = schema.name
|
|
48
|
+
indexes = self._unique_indexes.setdefault(rec_name, {})
|
|
49
|
+
for field in schema.unique_fields:
|
|
50
|
+
value = record.get(field)
|
|
51
|
+
if value is None:
|
|
52
|
+
continue
|
|
53
|
+
idx = indexes.setdefault(field, {})
|
|
54
|
+
if value in idx:
|
|
55
|
+
return field
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
def list_records(self, schema: RecordSchema, limit: int = 20) -> List[dict]:
|
|
59
|
+
records = list(self._data.get(schema.name, []))
|
|
60
|
+
key_order = "id" if "id" in schema.field_map else "_id"
|
|
61
|
+
records.sort(key=lambda rec: rec.get(key_order, 0))
|
|
62
|
+
return records[:limit]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
from namel3ss.errors.base import Namel3ssError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def execute_tool(name: str, args: Dict[str, object]) -> Dict[str, object]:
|
|
9
|
+
if name == "echo":
|
|
10
|
+
if not isinstance(args, dict):
|
|
11
|
+
raise Namel3ssError("Tool args must be a dictionary")
|
|
12
|
+
return {"echo": args}
|
|
13
|
+
raise Namel3ssError(f"Unknown tool '{name}'")
|