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.
- nighthawk/__init__.py +48 -0
- nighthawk/backends/__init__.py +0 -0
- nighthawk/backends/base.py +95 -0
- nighthawk/backends/claude_code_cli.py +342 -0
- nighthawk/backends/claude_code_sdk.py +325 -0
- nighthawk/backends/codex.py +352 -0
- nighthawk/backends/mcp_boundary.py +129 -0
- nighthawk/backends/mcp_server.py +226 -0
- nighthawk/backends/tool_bridge.py +240 -0
- nighthawk/configuration.py +193 -0
- nighthawk/errors.py +25 -0
- nighthawk/identifier_path.py +35 -0
- nighthawk/json_renderer.py +216 -0
- nighthawk/natural/__init__.py +0 -0
- nighthawk/natural/blocks.py +279 -0
- nighthawk/natural/decorator.py +302 -0
- nighthawk/natural/transform.py +346 -0
- nighthawk/runtime/__init__.py +0 -0
- nighthawk/runtime/async_bridge.py +50 -0
- nighthawk/runtime/prompt.py +344 -0
- nighthawk/runtime/runner.py +462 -0
- nighthawk/runtime/scoping.py +288 -0
- nighthawk/runtime/step_context.py +171 -0
- nighthawk/runtime/step_contract.py +231 -0
- nighthawk/runtime/step_executor.py +360 -0
- nighthawk/runtime/tool_calls.py +99 -0
- nighthawk/tools/__init__.py +0 -0
- nighthawk/tools/assignment.py +246 -0
- nighthawk/tools/contracts.py +72 -0
- nighthawk/tools/execution.py +83 -0
- nighthawk/tools/provided.py +80 -0
- nighthawk/tools/registry.py +212 -0
- nighthawk_python-0.1.0.dist-info/METADATA +111 -0
- nighthawk_python-0.1.0.dist-info/RECORD +36 -0
- nighthawk_python-0.1.0.dist-info/WHEEL +4 -0
- nighthawk_python-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
import tiktoken
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from ..json_renderer import JsonRendererStyle, render_json_text
|
|
9
|
+
|
|
10
|
+
type ErrorKind = Literal["invalid_input", "resolution", "execution", "transient", "internal"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ToolBoundaryError(Exception):
|
|
14
|
+
def __init__(self, *, kind: ErrorKind, message: str, guidance: str | None = None) -> None:
|
|
15
|
+
super().__init__(message)
|
|
16
|
+
self.kind: ErrorKind = kind
|
|
17
|
+
self.guidance: str | None = guidance
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _Error(BaseModel, extra="forbid"):
|
|
21
|
+
kind: ErrorKind
|
|
22
|
+
message: str
|
|
23
|
+
guidance: str | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ToolResult[ValueType](BaseModel, extra="forbid"):
|
|
27
|
+
value: ValueType | None
|
|
28
|
+
error: _Error | None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def render_tool_result_json_text(
|
|
32
|
+
*,
|
|
33
|
+
value: object | None,
|
|
34
|
+
error: object | None,
|
|
35
|
+
max_tokens: int,
|
|
36
|
+
encoding: tiktoken.Encoding,
|
|
37
|
+
style: JsonRendererStyle,
|
|
38
|
+
) -> str:
|
|
39
|
+
"""Render a tool result envelope as compact JSON text.
|
|
40
|
+
|
|
41
|
+
Both ``value`` and ``error`` are individually rendered under their own
|
|
42
|
+
token budgets via ``render_json_text``, then assembled into a
|
|
43
|
+
``{"value": ..., "error": ...}`` envelope using f-string interpolation.
|
|
44
|
+
The sub-values are already valid JSON fragments produced by
|
|
45
|
+
``render_json_text`` / ``json.dumps``, so the f-string concatenation
|
|
46
|
+
is structurally safe.
|
|
47
|
+
"""
|
|
48
|
+
if error is None:
|
|
49
|
+
error_text = "null"
|
|
50
|
+
error_token_count = 0
|
|
51
|
+
value_max_tokens = max_tokens
|
|
52
|
+
else:
|
|
53
|
+
error_max_tokens = int(max_tokens * 0.9)
|
|
54
|
+
error_text, error_token_count = render_json_text(
|
|
55
|
+
error,
|
|
56
|
+
max_tokens=error_max_tokens,
|
|
57
|
+
encoding=encoding,
|
|
58
|
+
style=style,
|
|
59
|
+
)
|
|
60
|
+
value_max_tokens = max(max_tokens - error_token_count, 0)
|
|
61
|
+
|
|
62
|
+
if value is None:
|
|
63
|
+
value_text = "null"
|
|
64
|
+
else:
|
|
65
|
+
value_text, _ = render_json_text(
|
|
66
|
+
value,
|
|
67
|
+
max_tokens=value_max_tokens,
|
|
68
|
+
encoding=encoding,
|
|
69
|
+
style=style,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return f'{{"value":{value_text},"error":{error_text}}}'
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Tool execution wrappers: normalization, classification, and toolset wrapping."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic_ai import RunContext
|
|
9
|
+
from pydantic_ai.exceptions import ApprovalRequired, CallDeferred, ModelRetry
|
|
10
|
+
from pydantic_ai.toolsets.abstract import ToolsetTool
|
|
11
|
+
from pydantic_ai.toolsets.wrapper import WrapperToolset
|
|
12
|
+
|
|
13
|
+
from ..json_renderer import JsonableValue, to_jsonable_value
|
|
14
|
+
from .contracts import ErrorKind, ToolBoundaryError, ToolResult, _Error
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _classify_unexpected_exception(exception: BaseException) -> ErrorKind:
|
|
18
|
+
if isinstance(exception, TimeoutError):
|
|
19
|
+
return "transient"
|
|
20
|
+
return "internal"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _normalize_tool_success(value: object) -> ToolResult[JsonableValue]:
|
|
24
|
+
return ToolResult(value=to_jsonable_value(value), error=None)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _normalize_tool_failure(*, kind: ErrorKind, message: str, guidance: str | None) -> ToolResult[JsonableValue]:
|
|
28
|
+
return ToolResult(
|
|
29
|
+
value=None,
|
|
30
|
+
error=_Error(
|
|
31
|
+
kind=kind,
|
|
32
|
+
message=message,
|
|
33
|
+
guidance=guidance,
|
|
34
|
+
),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def _run_tool_and_normalize(tool_call: Callable[[], Awaitable[object]]) -> ToolResult[JsonableValue]:
|
|
39
|
+
"""Execute a tool call and normalize the outcome to a ToolResult.
|
|
40
|
+
|
|
41
|
+
Control-flow exceptions are re-raised unchanged.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
value = await tool_call()
|
|
46
|
+
except (ModelRetry, CallDeferred, ApprovalRequired):
|
|
47
|
+
raise
|
|
48
|
+
except ToolBoundaryError as exception:
|
|
49
|
+
return _normalize_tool_failure(
|
|
50
|
+
kind=exception.kind,
|
|
51
|
+
message=str(exception),
|
|
52
|
+
guidance=exception.guidance,
|
|
53
|
+
)
|
|
54
|
+
except TimeoutError:
|
|
55
|
+
raise ModelRetry("Tool execution timed out. Retry.") from None
|
|
56
|
+
except Exception as exception:
|
|
57
|
+
kind = _classify_unexpected_exception(exception)
|
|
58
|
+
return _normalize_tool_failure(
|
|
59
|
+
kind=kind,
|
|
60
|
+
message=str(exception) or "Tool execution failed",
|
|
61
|
+
guidance="The tool execution raised an unexpected error. Retry or report this error.",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return _normalize_tool_success(value)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ToolResultWrapperToolset[DepsType](WrapperToolset[DepsType]):
|
|
68
|
+
def __getattr__(self, name: str) -> object:
|
|
69
|
+
return getattr(self.wrapped, name)
|
|
70
|
+
|
|
71
|
+
async def call_tool(
|
|
72
|
+
self,
|
|
73
|
+
name: str,
|
|
74
|
+
tool_args: dict[str, Any],
|
|
75
|
+
ctx: RunContext[DepsType],
|
|
76
|
+
tool: ToolsetTool[DepsType],
|
|
77
|
+
) -> ToolResult[JsonableValue]:
|
|
78
|
+
run_context = ctx
|
|
79
|
+
|
|
80
|
+
async def tool_call() -> object:
|
|
81
|
+
return await self.wrapped.call_tool(name, tool_args, run_context, tool)
|
|
82
|
+
|
|
83
|
+
return await _run_tool_and_normalize(tool_call)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic_ai import RunContext
|
|
7
|
+
from pydantic_ai.tools import Tool
|
|
8
|
+
|
|
9
|
+
from ..runtime.step_context import StepContext
|
|
10
|
+
from .assignment import assign_tool, eval_expression
|
|
11
|
+
from .contracts import ToolBoundaryError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class ProvidedToolDefinition:
|
|
16
|
+
name: str
|
|
17
|
+
tool: Tool[StepContext]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _eval_expression_or_raise(run_context: RunContext[StepContext], expression: str) -> object:
|
|
21
|
+
try:
|
|
22
|
+
return eval_expression(run_context.deps, expression)
|
|
23
|
+
except Exception as exception:
|
|
24
|
+
raise ToolBoundaryError(kind="execution", message=str(exception), guidance="Fix the expression and retry.") from exception
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def build_provided_tool_definitions() -> list[ProvidedToolDefinition]:
|
|
28
|
+
metadata = {"nighthawk.provided": True}
|
|
29
|
+
|
|
30
|
+
def nh_assign(
|
|
31
|
+
run_context: RunContext[StepContext],
|
|
32
|
+
target_path: str,
|
|
33
|
+
expression: str,
|
|
34
|
+
) -> dict[str, Any]:
|
|
35
|
+
return assign_tool(
|
|
36
|
+
run_context.deps,
|
|
37
|
+
target_path,
|
|
38
|
+
expression,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def nh_eval(run_context: RunContext[StepContext], expression: str) -> object:
|
|
42
|
+
return _eval_expression_or_raise(run_context, expression)
|
|
43
|
+
|
|
44
|
+
# nh_exec intentionally shares the same implementation as nh_eval.
|
|
45
|
+
# Both use Python eval(); the two-tool split exists purely as a semantic
|
|
46
|
+
# signal to the LLM (inspect vs mutate intent), not a runtime distinction.
|
|
47
|
+
# If future requirements demand runtime differentiation (e.g., read-only
|
|
48
|
+
# enforcement for nh_eval), the split provides the structural hook for it.
|
|
49
|
+
def nh_exec(run_context: RunContext[StepContext], expression: str) -> object:
|
|
50
|
+
return _eval_expression_or_raise(run_context, expression)
|
|
51
|
+
|
|
52
|
+
return [
|
|
53
|
+
ProvidedToolDefinition(
|
|
54
|
+
name="nh_assign",
|
|
55
|
+
tool=Tool(
|
|
56
|
+
nh_assign,
|
|
57
|
+
name="nh_assign",
|
|
58
|
+
metadata=metadata,
|
|
59
|
+
description="Rebind a name or set a nested field to a new value. target_path format: name(.field)*.",
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
ProvidedToolDefinition(
|
|
63
|
+
name="nh_eval",
|
|
64
|
+
tool=Tool(
|
|
65
|
+
nh_eval,
|
|
66
|
+
name="nh_eval",
|
|
67
|
+
metadata=metadata,
|
|
68
|
+
description="Evaluate a Python expression and return the result.",
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
ProvidedToolDefinition(
|
|
72
|
+
name="nh_exec",
|
|
73
|
+
tool=Tool(
|
|
74
|
+
nh_exec,
|
|
75
|
+
name="nh_exec",
|
|
76
|
+
metadata=metadata,
|
|
77
|
+
description="Execute a Python expression for its side effect (e.g., list.append(), dict.update()). Returns the expression result.",
|
|
78
|
+
),
|
|
79
|
+
),
|
|
80
|
+
]
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Callable, Iterator
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any, overload
|
|
9
|
+
|
|
10
|
+
from pydantic_ai.tools import Tool
|
|
11
|
+
|
|
12
|
+
from ..errors import ToolRegistrationError
|
|
13
|
+
from ..runtime.step_context import StepContext
|
|
14
|
+
from .provided import build_provided_tool_definitions
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ToolDefinition:
|
|
19
|
+
name: str
|
|
20
|
+
tool: Tool[StepContext]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_builtin_tool_name_to_definition: dict[str, ToolDefinition] = {}
|
|
24
|
+
_builtin_tools_registered = False
|
|
25
|
+
|
|
26
|
+
_global_tool_name_to_definition: dict[str, ToolDefinition] = {}
|
|
27
|
+
|
|
28
|
+
_tool_scope_stack_var: ContextVar[tuple[dict[str, ToolDefinition], ...]] = ContextVar(
|
|
29
|
+
"nighthawk_tool_scope_stack",
|
|
30
|
+
default=(),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
_call_scope_stack_var: ContextVar[tuple[dict[str, ToolDefinition], ...]] = ContextVar(
|
|
34
|
+
"nighthawk_call_tool_scope_stack",
|
|
35
|
+
default=(),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
_VALID_NAME_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _validate_tool_name(name: str) -> None:
|
|
42
|
+
try:
|
|
43
|
+
name.encode("ascii")
|
|
44
|
+
except UnicodeEncodeError as e:
|
|
45
|
+
raise ToolRegistrationError(f"Tool name must be ASCII: {name!r}") from e
|
|
46
|
+
|
|
47
|
+
if not _VALID_NAME_PATTERN.fullmatch(name):
|
|
48
|
+
raise ToolRegistrationError(f"Tool name must match ^[A-Za-z_][A-Za-z0-9_]*$: {name!r}")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def ensure_builtin_tools_registered() -> None:
|
|
52
|
+
global _builtin_tools_registered
|
|
53
|
+
|
|
54
|
+
if _builtin_tools_registered:
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
for builtin_definition in build_provided_tool_definitions():
|
|
58
|
+
_validate_tool_name(builtin_definition.name)
|
|
59
|
+
if builtin_definition.name in _builtin_tool_name_to_definition:
|
|
60
|
+
raise ToolRegistrationError(f"Duplicate builtin tool name: {builtin_definition.name!r}")
|
|
61
|
+
_builtin_tool_name_to_definition[builtin_definition.name] = ToolDefinition(
|
|
62
|
+
name=builtin_definition.name,
|
|
63
|
+
tool=builtin_definition.tool,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
_builtin_tools_registered = True
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _visible_tool_definitions() -> dict[str, ToolDefinition]:
|
|
70
|
+
ensure_builtin_tools_registered()
|
|
71
|
+
|
|
72
|
+
merged: dict[str, ToolDefinition] = dict(_builtin_tool_name_to_definition)
|
|
73
|
+
merged.update(_global_tool_name_to_definition)
|
|
74
|
+
|
|
75
|
+
for scope in _tool_scope_stack_var.get():
|
|
76
|
+
merged.update(scope)
|
|
77
|
+
|
|
78
|
+
for scope in _call_scope_stack_var.get():
|
|
79
|
+
merged.update(scope)
|
|
80
|
+
|
|
81
|
+
return merged
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _register_tool_definition(tool_definition: ToolDefinition, *, overwrite: bool) -> None:
|
|
85
|
+
ensure_builtin_tools_registered()
|
|
86
|
+
|
|
87
|
+
name = tool_definition.name
|
|
88
|
+
visible = _visible_tool_definitions()
|
|
89
|
+
|
|
90
|
+
if name in visible and not overwrite:
|
|
91
|
+
raise ToolRegistrationError(f"Tool name conflict: {name!r}. Pass overwrite=True to replace the visible definition.")
|
|
92
|
+
|
|
93
|
+
call_scope_stack = _call_scope_stack_var.get()
|
|
94
|
+
if call_scope_stack:
|
|
95
|
+
call_scope_stack[-1][name] = tool_definition
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
tool_scope_stack = _tool_scope_stack_var.get()
|
|
99
|
+
if tool_scope_stack:
|
|
100
|
+
tool_scope_stack[-1][name] = tool_definition
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
_global_tool_name_to_definition[name] = tool_definition
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@contextmanager
|
|
107
|
+
def tool_scope() -> Iterator[None]:
|
|
108
|
+
current = _tool_scope_stack_var.get()
|
|
109
|
+
token = _tool_scope_stack_var.set((*current, {}))
|
|
110
|
+
try:
|
|
111
|
+
yield
|
|
112
|
+
finally:
|
|
113
|
+
_tool_scope_stack_var.reset(token)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@contextmanager
|
|
117
|
+
def call_scope() -> Iterator[None]:
|
|
118
|
+
current = _call_scope_stack_var.get()
|
|
119
|
+
token = _call_scope_stack_var.set((*current, {}))
|
|
120
|
+
try:
|
|
121
|
+
yield
|
|
122
|
+
finally:
|
|
123
|
+
_call_scope_stack_var.reset(token)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_visible_tools() -> list[Tool[StepContext]]:
|
|
127
|
+
ensure_builtin_tools_registered()
|
|
128
|
+
visible = dict(_visible_tool_definitions())
|
|
129
|
+
return [definition.tool for definition in visible.values()]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
type ToolFunction = Callable[..., Any]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@overload
|
|
136
|
+
def tool(func: ToolFunction, /) -> ToolFunction: ...
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@overload
|
|
140
|
+
def tool(
|
|
141
|
+
func: None = None,
|
|
142
|
+
/,
|
|
143
|
+
*,
|
|
144
|
+
name: str | None = None,
|
|
145
|
+
overwrite: bool = False,
|
|
146
|
+
description: str | None = None,
|
|
147
|
+
metadata: dict[str, Any] | None = None,
|
|
148
|
+
) -> Callable[[ToolFunction], ToolFunction]: ...
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def tool(
|
|
152
|
+
func: ToolFunction | None = None,
|
|
153
|
+
/,
|
|
154
|
+
*,
|
|
155
|
+
name: str | None = None,
|
|
156
|
+
overwrite: bool = False,
|
|
157
|
+
description: str | None = None,
|
|
158
|
+
metadata: dict[str, Any] | None = None,
|
|
159
|
+
) -> ToolFunction | Callable[[ToolFunction], ToolFunction]:
|
|
160
|
+
"""Register a Python function as a Nighthawk tool visible to Natural blocks.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
func: The function to register. Can be omitted for use as a bare decorator.
|
|
164
|
+
name: Tool name override. Defaults to the function name.
|
|
165
|
+
overwrite: If True, replace any existing tool with the same name.
|
|
166
|
+
description: Tool description override. Defaults to the function docstring.
|
|
167
|
+
metadata: Arbitrary metadata attached to the tool definition.
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
ToolRegistrationError: If the name conflicts with an existing tool and
|
|
171
|
+
overwrite is False.
|
|
172
|
+
|
|
173
|
+
Example:
|
|
174
|
+
```python
|
|
175
|
+
@nighthawk.tool
|
|
176
|
+
def lookup_user(user_id: str) -> dict:
|
|
177
|
+
return {"user_id": user_id, "name": "Alice"}
|
|
178
|
+
```
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
def decorator(inner: ToolFunction) -> ToolFunction:
|
|
182
|
+
ensure_builtin_tools_registered()
|
|
183
|
+
|
|
184
|
+
tool_name = name or inner.__name__
|
|
185
|
+
_validate_tool_name(tool_name)
|
|
186
|
+
|
|
187
|
+
resolved_description = description
|
|
188
|
+
if resolved_description is None:
|
|
189
|
+
resolved_description = inner.__doc__
|
|
190
|
+
|
|
191
|
+
tool_object: Tool[StepContext] = Tool(
|
|
192
|
+
inner,
|
|
193
|
+
name=tool_name,
|
|
194
|
+
description=resolved_description,
|
|
195
|
+
metadata=metadata,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
tool_definition = ToolDefinition(name=tool_name, tool=tool_object)
|
|
199
|
+
_register_tool_definition(tool_definition, overwrite=overwrite)
|
|
200
|
+
return inner
|
|
201
|
+
|
|
202
|
+
if func is not None:
|
|
203
|
+
return decorator(func)
|
|
204
|
+
|
|
205
|
+
return decorator
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _reset_all_tools_for_tests() -> None:
|
|
209
|
+
global _builtin_tools_registered
|
|
210
|
+
_global_tool_name_to_definition.clear()
|
|
211
|
+
_builtin_tool_name_to_definition.clear()
|
|
212
|
+
_builtin_tools_registered = False
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nighthawk-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An experimental Python library that embeds Natural blocks inside Python functions and executes them using an LLM.
|
|
5
|
+
Project-URL: Repository, https://github.com/kurusugawa-computer/nighthawk-python
|
|
6
|
+
Project-URL: Documentation, https://kurusugawa-computer.github.io/nighthawk-python/
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/kurusugawa-computer/nighthawk-python/issues
|
|
8
|
+
Author-email: "Kurusugawa Computer Inc." <oss@kurusugawa.jp>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: embedded-dsl,interoperability,llm,natural-language,pydantic-ai
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.13
|
|
21
|
+
Requires-Dist: headson>=0.16.1
|
|
22
|
+
Requires-Dist: pydantic-ai-slim>=1.59
|
|
23
|
+
Requires-Dist: pydantic>=2
|
|
24
|
+
Requires-Dist: pyyaml>=6
|
|
25
|
+
Requires-Dist: tiktoken>=0.12
|
|
26
|
+
Provides-Extra: claude-code-cli
|
|
27
|
+
Requires-Dist: mcp>=1.26; extra == 'claude-code-cli'
|
|
28
|
+
Provides-Extra: claude-code-sdk
|
|
29
|
+
Requires-Dist: claude-agent-sdk>=0.1; extra == 'claude-code-sdk'
|
|
30
|
+
Provides-Extra: codex
|
|
31
|
+
Requires-Dist: mcp>=1.26; extra == 'codex'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
[](https://pypi.org/project/nighthawk-python)
|
|
35
|
+

|
|
36
|
+
[](https://github.com/kurusugawa-computer/nighthawk-python/tree/main/LICENSE)
|
|
37
|
+
[](https://github.com/kurusugawa-computer/nighthawk-python/issues)
|
|
38
|
+
|
|
39
|
+
# Nighthawk
|
|
40
|
+
|
|
41
|
+
<div align="center">
|
|
42
|
+
<img src="https://github.com/kurusugawa-computer/nighthawk-python/raw/main/docs/assets/nighthawk_logo-128x128.png" alt="nighthawk-logo" width="128px" margin="10px"></img>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
Nighthawk is an experimental Python library exploring a clear separation between **hard control** (Python code) for strict procedure and deterministic flow, and **soft reasoning** (an LLM) for semantic interpretation inside small embedded "Natural blocks". It is a compact reimplementation of the core ideas of [Nightjar](https://github.com/psg-mit/nightjarpy).
|
|
46
|
+
|
|
47
|
+
## Quickstart
|
|
48
|
+
|
|
49
|
+
Prerequisites: Python 3.13+
|
|
50
|
+
|
|
51
|
+
Install Nighthawk and a provider:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install nighthawk-python pydantic-ai-slim[openai]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Save as `quickstart.py`:
|
|
58
|
+
|
|
59
|
+
```py
|
|
60
|
+
import nighthawk as nh
|
|
61
|
+
|
|
62
|
+
step_executor = nh.AgentStepExecutor.from_configuration(
|
|
63
|
+
configuration=nh.StepExecutorConfiguration(model="openai-responses:gpt-5-mini")
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
with nh.run(step_executor):
|
|
67
|
+
|
|
68
|
+
@nh.natural_function
|
|
69
|
+
def calculate_total(items: str) -> int:
|
|
70
|
+
total = 0
|
|
71
|
+
"""natural
|
|
72
|
+
Read <items> and set <:total> to the sum of all quantities mentioned.
|
|
73
|
+
"""
|
|
74
|
+
return total
|
|
75
|
+
|
|
76
|
+
print(calculate_total("three apples, a dozen eggs, and 5 oranges"))
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Run with your API key:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
export OPENAI_API_KEY=sk-xxxxxxxxx
|
|
83
|
+
python quickstart.py
|
|
84
|
+
# => 20
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
For backends, credentials, model identifiers, and detailed guidance, see the [documentation site](https://kurusugawa-computer.github.io/nighthawk-python/).
|
|
88
|
+
|
|
89
|
+
## Development
|
|
90
|
+
|
|
91
|
+
Run tests:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
uv run pytest -q
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Run an OTel collector UI (otel-tui) for observability:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
docker run --rm -it -p 4318:4318 --name otel-tui ymtdzzz/otel-tui:latest
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Then run integration tests with `OTEL_EXPORTER_OTLP_ENDPOINT` set:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 uv run pytest -q tests/integration/test_llm_integration.py
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## References
|
|
110
|
+
|
|
111
|
+
- Nightjar (upstream concept): https://github.com/psg-mit/nightjarpy
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
nighthawk/__init__.py,sha256=4Yt9le9-Nh0xZ9Ty9t5XNZY8_KBTUZtKz7zBe8UadHA,1296
|
|
2
|
+
nighthawk/configuration.py,sha256=igHa9H1eaHT4D8jtpd1i28O3f_CRc8zkwtxQ93WsQyY,7241
|
|
3
|
+
nighthawk/errors.py,sha256=CSw_VfIoppCE2SdN2zMtVbMGu_wFwD628v8ZAVxeHIY,602
|
|
4
|
+
nighthawk/identifier_path.py,sha256=Io3VcT8BlxAASHQUSUclrRcCwnRKTtMDPSR1bpaDN6s,984
|
|
5
|
+
nighthawk/json_renderer.py,sha256=ixpUYTsVVKOp-iMdHldw7-6I3JxelXOQD8vK2KVXwbw,7640
|
|
6
|
+
nighthawk/backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
nighthawk/backends/base.py,sha256=HicDmOFN_CChCDs532dHtiGAiw1wKOsWCx0185u1Dz8,3869
|
|
8
|
+
nighthawk/backends/claude_code_cli.py,sha256=g1Jy1JtBaETyL1lMSSUfwwnPV_ojJTCokdVlG8EQn5A,14436
|
|
9
|
+
nighthawk/backends/claude_code_sdk.py,sha256=-EtGp5J5x8wncWaPJ-REBvtZVfsKQpYv5-F9lTGN-ck,13176
|
|
10
|
+
nighthawk/backends/codex.py,sha256=nSRt6gsssGi-xQ9tD7xvkC8aYH98itYzhIbw4K0Klis,14739
|
|
11
|
+
nighthawk/backends/mcp_boundary.py,sha256=DjBgYI32bRne3d91XxSV56xBrxOjcnJi2nyxGcy613E,4186
|
|
12
|
+
nighthawk/backends/mcp_server.py,sha256=Vw1iRRhrMgFlX5umSLIc0XGlRAljOm9xqoTYBNpbjxI,8142
|
|
13
|
+
nighthawk/backends/tool_bridge.py,sha256=yv4WbxT7OQQaR3OsHuNIPfKhNjsaIIfRFbqv2hNtMro,9288
|
|
14
|
+
nighthawk/natural/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
nighthawk/natural/blocks.py,sha256=U6VhskMuEVUdWiPqFUBavFlvHgDkDbCYOKeQpdXxXMY,9717
|
|
16
|
+
nighthawk/natural/decorator.py,sha256=3bx4PQ85Odh5sN3_S3N9wNdDmFUWxUgzF_YbfpNS-LM,11134
|
|
17
|
+
nighthawk/natural/transform.py,sha256=_Uctcz65kI7yN_uuWmG-qIujQzgd7sqq-IGdlzEAopg,14950
|
|
18
|
+
nighthawk/runtime/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
nighthawk/runtime/async_bridge.py,sha256=k1s7e5dJEMNZJd0xHqkqAY7VkE7tOBzJf6nAEJH_Xh0,1478
|
|
20
|
+
nighthawk/runtime/prompt.py,sha256=Q5IfNCj0II415uevGkNJyRZPvEBz16_BRk6KMIxOve4,12254
|
|
21
|
+
nighthawk/runtime/runner.py,sha256=B20ZHt4DCvqsYwicVsKPc9cmitcNUMIzW1zzYRfLwgY,17946
|
|
22
|
+
nighthawk/runtime/scoping.py,sha256=yKYjgSXRp4kXvmOE3ai34_dbAkFhc1VGbwyvri81BmU,9474
|
|
23
|
+
nighthawk/runtime/step_context.py,sha256=CPfCOOc1KWvF_9nzmKOmm2NssL5-FXoDhzI4iWwcomg,5489
|
|
24
|
+
nighthawk/runtime/step_contract.py,sha256=DFKH8wuEA6ZY9MKHw6lwQhD6-cgq6sJAjUBSfOw1_-Y,7312
|
|
25
|
+
nighthawk/runtime/step_executor.py,sha256=7b9mYA-NqkR9CXNyHbZWCYb1q6k8cr_UXoy0CyhZXVY,13028
|
|
26
|
+
nighthawk/runtime/tool_calls.py,sha256=Hepb71WF_k51_PSnXtyiAoSm53kDBbqLsqd7Lh-5nWE,3056
|
|
27
|
+
nighthawk/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
+
nighthawk/tools/assignment.py,sha256=Mh-WLK7U5pE3IBoyzXVJfFW5SdAxqUL0O1NSFkiX4ds,8823
|
|
29
|
+
nighthawk/tools/contracts.py,sha256=lLfBue1VF8NMd76GMIp6y71cCx1p4MlVQyNhd4Ju2qk,2096
|
|
30
|
+
nighthawk/tools/execution.py,sha256=e1uv09MWOkCoZ5uQu9myUXyhQzM_UziR9yE0raFYtQI,2740
|
|
31
|
+
nighthawk/tools/provided.py,sha256=6vIBnT33m0KUNQ40xgnhm52wAtV1afPEYVrgIc-ZiUk,2773
|
|
32
|
+
nighthawk/tools/registry.py,sha256=9obdHJmocr9b_DGBVzq8unDQA1jy-OfMWRky0UGsLQA,6141
|
|
33
|
+
nighthawk_python-0.1.0.dist-info/METADATA,sha256=K2XHWwrdiSZ_aVssgQdKLWaTjoPuXqIRYJko4AtK3R4,3967
|
|
34
|
+
nighthawk_python-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
35
|
+
nighthawk_python-0.1.0.dist-info/licenses/LICENSE,sha256=R8tjrX79o5Pbi_l_aFPPWDbTZkmz4is0CxmzMMs3f0A,1076
|
|
36
|
+
nighthawk_python-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright 2026 Kurusugawa Computer Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|