nighthawk-python 0.1.0__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.
@@ -0,0 +1,462 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import uuid
5
+ from dataclasses import dataclass
6
+ from types import FrameType
7
+ from typing import TypedDict
8
+
9
+ from pydantic import TypeAdapter
10
+
11
+ from ..errors import ExecutionError, NaturalParseError
12
+ from ..identifier_path import parse_identifier_path
13
+ from ..natural.blocks import parse_frontmatter, validate_frontmatter_deny
14
+ from .async_bridge import run_coroutine_synchronously
15
+ from .scoping import RUN_ID, SCOPE_ID, STEP_ID, get_execution_context, span
16
+ from .step_context import (
17
+ _MISSING,
18
+ StepContext,
19
+ ToolResultRenderingPolicy,
20
+ get_python_cell_scope_stack,
21
+ get_python_name_scope_stack,
22
+ get_step_context_stack,
23
+ resolve_name_in_step_context,
24
+ )
25
+ from .step_contract import (
26
+ RaiseStepOutcome,
27
+ ReturnStepOutcome,
28
+ StepOutcome,
29
+ )
30
+ from .step_executor import AsyncStepExecutor, StepExecutor, SyncStepExecutor
31
+
32
+
33
+ def _split_frontmatter(
34
+ processed_natural_program: str,
35
+ ) -> tuple[str, tuple[str, ...]]:
36
+ """Parse frontmatter, validate deny directives, and return stripped program + denied kinds."""
37
+ try:
38
+ program_without_frontmatter, frontmatter = parse_frontmatter(processed_natural_program)
39
+ except NaturalParseError as e:
40
+ raise ExecutionError(str(e)) from e
41
+ try:
42
+ denied_step_kinds = validate_frontmatter_deny(frontmatter)
43
+ except NaturalParseError as e:
44
+ raise ExecutionError(str(e)) from e
45
+ return program_without_frontmatter, denied_step_kinds
46
+
47
+
48
+ def _compute_allowed_step_kinds(is_in_loop: bool, denied_step_kinds: tuple[str, ...]) -> tuple[str, ...]:
49
+ base_allowed_kinds: list[str] = ["pass", "return", "raise"]
50
+ if is_in_loop:
51
+ base_allowed_kinds.extend(["break", "continue"])
52
+ return tuple(kind for kind in base_allowed_kinds if kind not in denied_step_kinds)
53
+
54
+
55
+ def _build_step_globals(
56
+ python_globals: dict[str, object],
57
+ ) -> dict[str, object]:
58
+ step_globals: dict[str, object] = dict(python_globals)
59
+ if "__builtins__" not in step_globals:
60
+ step_globals["__builtins__"] = __builtins__
61
+ return step_globals
62
+
63
+
64
+ def _build_step_locals(
65
+ python_locals: dict[str, object],
66
+ ) -> dict[str, object]:
67
+ step_locals: dict[str, object] = {}
68
+ step_context_stack = get_step_context_stack()
69
+ if step_context_stack:
70
+ step_locals.update(step_context_stack[-1].step_locals)
71
+ step_locals.update(python_locals)
72
+ return step_locals
73
+
74
+
75
+ def _resolve_input_bindings(
76
+ input_binding_names: list[str],
77
+ *,
78
+ python_locals: dict[str, object],
79
+ python_globals: dict[str, object],
80
+ caller_frame: FrameType,
81
+ ) -> dict[str, tuple[object, str]]:
82
+ """Resolve each input binding using Python LEGB rules.
83
+
84
+ Returns a mapping of binding name to (resolved_value, resolution_kind).
85
+ """
86
+ local_variable_name_set = set(caller_frame.f_code.co_varnames)
87
+ local_variable_name_set.update(caller_frame.f_code.co_cellvars)
88
+ free_variable_name_set = set(caller_frame.f_code.co_freevars)
89
+
90
+ python_cell_scope_stack = get_python_cell_scope_stack()
91
+ python_name_scope_stack = get_python_name_scope_stack()
92
+
93
+ python_builtins = python_globals.get("__builtins__", __builtins__)
94
+
95
+ def resolve_one(binding_name: str) -> tuple[object, str]:
96
+ if binding_name in python_locals:
97
+ return python_locals[binding_name], "locals"
98
+
99
+ for scope in reversed(python_cell_scope_stack):
100
+ if binding_name not in scope:
101
+ continue
102
+ cell = scope[binding_name]
103
+ try:
104
+ return cell.cell_contents, "cell_scope"
105
+ except ValueError:
106
+ break
107
+
108
+ if binding_name in local_variable_name_set:
109
+ raise UnboundLocalError(f"cannot access local variable {binding_name!r} where it is not associated with a value")
110
+
111
+ if binding_name in free_variable_name_set:
112
+ error = NameError(f"cannot access free variable {binding_name!r} where it is not associated with a value in enclosing scope")
113
+ error.name = binding_name
114
+ raise error
115
+
116
+ for scope in reversed(python_name_scope_stack):
117
+ if binding_name in scope:
118
+ return scope[binding_name], "name_scope"
119
+
120
+ if binding_name in python_globals:
121
+ return python_globals[binding_name], "globals"
122
+
123
+ if isinstance(python_builtins, dict) and binding_name in python_builtins:
124
+ return python_builtins[binding_name], "builtins"
125
+
126
+ if hasattr(python_builtins, binding_name):
127
+ return getattr(python_builtins, binding_name), "builtins"
128
+
129
+ error = NameError(f"name {binding_name!r} is not defined")
130
+ error.name = binding_name
131
+ raise error
132
+
133
+ binding_name_to_value_and_resolution_kind: dict[str, tuple[object, str]] = {}
134
+ for binding_name in input_binding_names:
135
+ binding_name_to_value_and_resolution_kind[binding_name] = resolve_one(binding_name)
136
+ return binding_name_to_value_and_resolution_kind
137
+
138
+
139
+ class StepEnvelope(TypedDict):
140
+ """Envelope returned by Runner.run_step / run_step_async."""
141
+
142
+ step_outcome: StepOutcome
143
+ input_bindings: dict[str, object]
144
+ bindings: dict[str, object]
145
+ return_value: object | None
146
+
147
+
148
+ @dataclass(frozen=True)
149
+ class _StepPreparation:
150
+ """Result of preparing a Natural block for execution."""
151
+
152
+ processed_program: str
153
+ allowed_step_kinds: tuple[str, ...]
154
+ step_context: StepContext
155
+ input_binding_name_to_value: dict[str, object]
156
+
157
+
158
+ class Runner:
159
+ def __init__(self, step_executor: StepExecutor) -> None:
160
+ self.step_executor = step_executor
161
+
162
+ def _parse_and_coerce_return_value(self, value: object, return_annotation: object) -> object:
163
+ try:
164
+ adapted = TypeAdapter(return_annotation)
165
+ return adapted.validate_python(value)
166
+ except Exception as e:
167
+ raise ExecutionError(f"Return value validation failed: {e}") from e
168
+
169
+ def _resolve_reference_path(self, step_context: StepContext, return_reference_path: str) -> object:
170
+ parsed_path = parse_identifier_path(return_reference_path)
171
+ if parsed_path is None:
172
+ raise ExecutionError(f"Invalid return_reference_path: {return_reference_path!r}")
173
+
174
+ root_name = parsed_path[0]
175
+ if root_name not in step_context.step_locals:
176
+ raise ExecutionError(f"Unknown root name in return_reference_path: {root_name}")
177
+ current = step_context.step_locals[root_name]
178
+
179
+ for part in parsed_path[1:]:
180
+ try:
181
+ current = getattr(current, part)
182
+ except Exception as e:
183
+ raise ExecutionError(f"Failed to resolve return_reference_path segment {part!r} in {return_reference_path!r}") from e
184
+
185
+ return current
186
+
187
+ def _prepare_step_execution(
188
+ self,
189
+ natural_program: str,
190
+ input_binding_names: list[str],
191
+ output_binding_names: list[str],
192
+ binding_name_to_type: dict[str, object],
193
+ is_in_loop: bool,
194
+ *,
195
+ caller_frame: FrameType,
196
+ ) -> _StepPreparation:
197
+ python_locals = caller_frame.f_locals
198
+ python_globals = caller_frame.f_globals
199
+
200
+ processed_without_frontmatter, denied_step_kinds = _split_frontmatter(natural_program)
201
+ processed_without_frontmatter = processed_without_frontmatter.lstrip("\n")
202
+
203
+ allowed_step_kinds = _compute_allowed_step_kinds(is_in_loop, denied_step_kinds)
204
+
205
+ step_globals = _build_step_globals(python_globals)
206
+ step_locals = _build_step_locals(python_locals)
207
+
208
+ resolved_bindings = _resolve_input_bindings(
209
+ input_binding_names,
210
+ python_locals=python_locals,
211
+ python_globals=python_globals,
212
+ caller_frame=caller_frame,
213
+ )
214
+
215
+ for binding_name, (value, resolution_kind) in resolved_bindings.items():
216
+ if resolution_kind in ("locals", "cell_scope", "name_scope"):
217
+ step_locals[binding_name] = value
218
+
219
+ tool_result_rendering_policy = getattr(self.step_executor, "tool_result_rendering_policy", None)
220
+ if tool_result_rendering_policy is not None and not isinstance(tool_result_rendering_policy, ToolResultRenderingPolicy):
221
+ raise ExecutionError("Step executor tool_result_rendering_policy must be ToolResultRenderingPolicy when provided")
222
+
223
+ binding_commit_targets = set(output_binding_names)
224
+ read_binding_names = frozenset(input_binding_names) - binding_commit_targets
225
+
226
+ step_context = StepContext(
227
+ step_id=str(uuid.uuid4()),
228
+ step_globals=step_globals,
229
+ step_locals=step_locals,
230
+ binding_commit_targets=binding_commit_targets,
231
+ read_binding_names=read_binding_names,
232
+ binding_name_to_type=binding_name_to_type,
233
+ tool_result_rendering_policy=tool_result_rendering_policy,
234
+ )
235
+
236
+ input_binding_name_to_value = {name: value for name, (value, _) in resolved_bindings.items()}
237
+ return _StepPreparation(
238
+ processed_program=processed_without_frontmatter,
239
+ allowed_step_kinds=allowed_step_kinds,
240
+ step_context=step_context,
241
+ input_binding_name_to_value=input_binding_name_to_value,
242
+ )
243
+
244
+ def _validate_raise_outcome(
245
+ self,
246
+ step_context: StepContext,
247
+ step_outcome: RaiseStepOutcome,
248
+ ) -> None:
249
+ if step_outcome.raise_error_type is not None:
250
+ resolved_raise_error_type = resolve_name_in_step_context(step_context, step_outcome.raise_error_type)
251
+ if resolved_raise_error_type is _MISSING:
252
+ raise ExecutionError(f"Invalid raise_error_type: {step_outcome.raise_error_type!r}: {step_outcome.raise_message}")
253
+ if not isinstance(resolved_raise_error_type, type) or not issubclass(resolved_raise_error_type, BaseException):
254
+ raise ExecutionError(f"Invalid raise_error_type: {step_outcome.raise_error_type!r}: {step_outcome.raise_message}")
255
+ raise resolved_raise_error_type(step_outcome.raise_message)
256
+
257
+ raise ExecutionError(f"Execution failed: {step_outcome.raise_message}")
258
+
259
+ def _apply_bindings_and_validate_kind(
260
+ self,
261
+ *,
262
+ step_context: StepContext,
263
+ step_outcome: StepOutcome,
264
+ bindings: dict[str, object],
265
+ allowed_step_kinds: tuple[str, ...],
266
+ ) -> str:
267
+ step_outcome_kind = step_outcome.kind
268
+ if step_outcome_kind not in allowed_step_kinds:
269
+ raise ExecutionError(f"Step '{step_outcome_kind}' is not allowed for this step. Allowed kinds: {allowed_step_kinds}")
270
+ step_context.step_locals.update(bindings)
271
+ return step_outcome_kind
272
+
273
+ def _finalize_step(
274
+ self,
275
+ *,
276
+ preparation: _StepPreparation,
277
+ step_outcome: StepOutcome,
278
+ bindings: dict[str, object],
279
+ return_annotation: object,
280
+ ) -> StepEnvelope:
281
+ """Sync finalization: validate kind, resolve return value, handle raise."""
282
+ step_outcome_kind = self._apply_bindings_and_validate_kind(
283
+ step_context=preparation.step_context,
284
+ step_outcome=step_outcome,
285
+ bindings=bindings,
286
+ allowed_step_kinds=preparation.allowed_step_kinds,
287
+ )
288
+
289
+ return_value: object | None = None
290
+ if step_outcome_kind == "return":
291
+ assert isinstance(step_outcome, ReturnStepOutcome)
292
+ resolved = self._resolve_reference_path(
293
+ preparation.step_context,
294
+ step_outcome.return_reference_path,
295
+ )
296
+ if inspect.isawaitable(resolved):
297
+ raise ExecutionError("Sync Natural function cannot return an awaitable value. Use async def and await the function call.")
298
+ return_value = self._parse_and_coerce_return_value(resolved, return_annotation)
299
+
300
+ if step_outcome_kind == "raise":
301
+ assert isinstance(step_outcome, RaiseStepOutcome)
302
+ self._validate_raise_outcome(preparation.step_context, step_outcome)
303
+
304
+ return StepEnvelope(
305
+ step_outcome=step_outcome,
306
+ input_bindings=dict(preparation.input_binding_name_to_value),
307
+ bindings=bindings,
308
+ return_value=return_value,
309
+ )
310
+
311
+ async def _run_step_async_impl(
312
+ self,
313
+ natural_program: str,
314
+ input_binding_names: list[str],
315
+ output_binding_names: list[str],
316
+ binding_name_to_type: dict[str, object],
317
+ return_annotation: object,
318
+ is_in_loop: bool,
319
+ *,
320
+ caller_frame: FrameType,
321
+ ) -> StepEnvelope:
322
+ preparation = self._prepare_step_execution(
323
+ natural_program,
324
+ input_binding_names,
325
+ output_binding_names,
326
+ binding_name_to_type,
327
+ is_in_loop,
328
+ caller_frame=caller_frame,
329
+ )
330
+ execution_context = get_execution_context()
331
+
332
+ with span(
333
+ "nighthawk.step",
334
+ **{
335
+ RUN_ID: execution_context.run_id,
336
+ SCOPE_ID: execution_context.scope_id,
337
+ STEP_ID: preparation.step_context.step_id,
338
+ },
339
+ ):
340
+ step_executor = self.step_executor
341
+
342
+ if isinstance(step_executor, AsyncStepExecutor):
343
+ step_outcome, bindings = await step_executor.run_step_async(
344
+ processed_natural_program=preparation.processed_program,
345
+ step_context=preparation.step_context,
346
+ binding_names=output_binding_names,
347
+ allowed_step_kinds=preparation.allowed_step_kinds,
348
+ )
349
+ elif isinstance(step_executor, SyncStepExecutor):
350
+ step_outcome, bindings = step_executor.run_step(
351
+ processed_natural_program=preparation.processed_program,
352
+ step_context=preparation.step_context,
353
+ binding_names=output_binding_names,
354
+ allowed_step_kinds=preparation.allowed_step_kinds,
355
+ )
356
+ else:
357
+ raise ExecutionError("Step executor must define run_step_async(...) or run_step(...)")
358
+
359
+ step_outcome_kind = self._apply_bindings_and_validate_kind(
360
+ step_context=preparation.step_context,
361
+ step_outcome=step_outcome,
362
+ bindings=bindings,
363
+ allowed_step_kinds=preparation.allowed_step_kinds,
364
+ )
365
+
366
+ return_value: object | None = None
367
+ if step_outcome_kind == "return":
368
+ assert isinstance(step_outcome, ReturnStepOutcome)
369
+ resolved = self._resolve_reference_path(
370
+ preparation.step_context,
371
+ step_outcome.return_reference_path,
372
+ )
373
+ if inspect.isawaitable(resolved):
374
+ resolved = await resolved
375
+ return_value = self._parse_and_coerce_return_value(resolved, return_annotation)
376
+
377
+ if step_outcome_kind == "raise":
378
+ assert isinstance(step_outcome, RaiseStepOutcome)
379
+ self._validate_raise_outcome(preparation.step_context, step_outcome)
380
+
381
+ return StepEnvelope(
382
+ step_outcome=step_outcome,
383
+ input_bindings=dict(preparation.input_binding_name_to_value),
384
+ bindings=bindings,
385
+ return_value=return_value,
386
+ )
387
+
388
+ def run_step(
389
+ self,
390
+ natural_program: str,
391
+ input_binding_names: list[str],
392
+ output_binding_names: list[str],
393
+ binding_name_to_type: dict[str, object],
394
+ return_annotation: object,
395
+ is_in_loop: bool,
396
+ *,
397
+ caller_frame: FrameType,
398
+ ) -> StepEnvelope:
399
+ preparation = self._prepare_step_execution(
400
+ natural_program,
401
+ input_binding_names,
402
+ output_binding_names,
403
+ binding_name_to_type,
404
+ is_in_loop,
405
+ caller_frame=caller_frame,
406
+ )
407
+
408
+ if isinstance(self.step_executor, SyncStepExecutor):
409
+ execution_context = get_execution_context()
410
+ with span(
411
+ "nighthawk.step",
412
+ **{
413
+ RUN_ID: execution_context.run_id,
414
+ SCOPE_ID: execution_context.scope_id,
415
+ STEP_ID: preparation.step_context.step_id,
416
+ },
417
+ ):
418
+ step_outcome, bindings = self.step_executor.run_step(
419
+ processed_natural_program=preparation.processed_program,
420
+ step_context=preparation.step_context,
421
+ binding_names=output_binding_names,
422
+ allowed_step_kinds=preparation.allowed_step_kinds,
423
+ )
424
+ return self._finalize_step(
425
+ preparation=preparation,
426
+ step_outcome=step_outcome,
427
+ bindings=bindings,
428
+ return_annotation=return_annotation,
429
+ )
430
+
431
+ return run_coroutine_synchronously(
432
+ lambda: self._run_step_async_impl(
433
+ natural_program,
434
+ input_binding_names,
435
+ output_binding_names,
436
+ binding_name_to_type,
437
+ return_annotation,
438
+ is_in_loop,
439
+ caller_frame=caller_frame,
440
+ )
441
+ )
442
+
443
+ async def run_step_async(
444
+ self,
445
+ natural_program: str,
446
+ input_binding_names: list[str],
447
+ output_binding_names: list[str],
448
+ binding_name_to_type: dict[str, object],
449
+ return_annotation: object,
450
+ is_in_loop: bool,
451
+ *,
452
+ caller_frame: FrameType,
453
+ ) -> StepEnvelope:
454
+ return await self._run_step_async_impl(
455
+ natural_program,
456
+ input_binding_names,
457
+ output_binding_names,
458
+ binding_name_to_type,
459
+ return_annotation,
460
+ is_in_loop,
461
+ caller_frame=caller_frame,
462
+ )