langchain-quickjs 0.0.1__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.
- langchain_quickjs/__init__.py +7 -0
- langchain_quickjs/_foreign_function_docs.py +447 -0
- langchain_quickjs/_foreign_functions.py +257 -0
- langchain_quickjs/middleware.py +235 -0
- langchain_quickjs-0.0.1.dist-info/METADATA +62 -0
- langchain_quickjs-0.0.1.dist-info/RECORD +8 -0
- langchain_quickjs-0.0.1.dist-info/WHEEL +4 -0
- langchain_quickjs-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"""Render compact prompt-facing documentation for QuickJS foreign functions.
|
|
2
|
+
|
|
3
|
+
QuickJS foreign functions are Python callables or LangChain tools exposed inside
|
|
4
|
+
JavaScript. The model needs a concise description of their names, argument
|
|
5
|
+
shapes, return types, and any referenced structured types. This module converts
|
|
6
|
+
Python signatures and docstrings into small TypeScript-like stubs and prompt
|
|
7
|
+
sections that are easy for the model to consume.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import contextlib
|
|
13
|
+
import inspect
|
|
14
|
+
from typing import TYPE_CHECKING, Any, get_args, get_origin, get_type_hints
|
|
15
|
+
|
|
16
|
+
from langchain_core.tools import BaseTool
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
|
|
21
|
+
_ELLIPSIS_TUPLE_ARG_COUNT = 2
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _format_basic_annotation(annotation: Any) -> str | None:
|
|
25
|
+
"""Render simple built-in annotations without container introspection."""
|
|
26
|
+
basic_types = {
|
|
27
|
+
Any: "any",
|
|
28
|
+
inspect.Signature.empty: "any",
|
|
29
|
+
str: "string",
|
|
30
|
+
bool: "boolean",
|
|
31
|
+
type(None): "null",
|
|
32
|
+
dict: "Record<string, any>",
|
|
33
|
+
}
|
|
34
|
+
if annotation in basic_types:
|
|
35
|
+
return basic_types[annotation]
|
|
36
|
+
if annotation in (int, float):
|
|
37
|
+
return "number"
|
|
38
|
+
if isinstance(annotation, type):
|
|
39
|
+
return annotation.__name__
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _format_collection_annotation(origin: Any, args: tuple[Any, ...]) -> str | None:
|
|
44
|
+
"""Render collection-like generic annotations."""
|
|
45
|
+
if origin in (list, set, frozenset):
|
|
46
|
+
item = _format_annotation(args[0]) if args else "any"
|
|
47
|
+
return f"{item}[]"
|
|
48
|
+
if origin is tuple:
|
|
49
|
+
if len(args) == _ELLIPSIS_TUPLE_ARG_COUNT and args[1] is Ellipsis:
|
|
50
|
+
return f"{_format_annotation(args[0])}[]"
|
|
51
|
+
return f"[{', '.join(_format_annotation(arg) for arg in args)}]"
|
|
52
|
+
if origin is dict:
|
|
53
|
+
key_type = _format_annotation(args[0]) if args else "string"
|
|
54
|
+
value_type = _format_annotation(args[1]) if len(args) > 1 else "any"
|
|
55
|
+
return f"Record<{key_type}, {value_type}>"
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _format_generic_annotation(origin: Any, args: tuple[Any, ...]) -> str | None:
|
|
60
|
+
"""Render non-collection generic annotations."""
|
|
61
|
+
if origin is type:
|
|
62
|
+
inner = _format_annotation(args[0]) if args else "any"
|
|
63
|
+
return f"new (...args: any[]) => {inner}"
|
|
64
|
+
origin_name = getattr(origin, "__name__", None)
|
|
65
|
+
if origin_name in {"Union", "UnionType"}:
|
|
66
|
+
return " | ".join(_format_annotation(arg) for arg in args)
|
|
67
|
+
if origin_name:
|
|
68
|
+
formatted_args = ", ".join(_format_annotation(arg) for arg in args)
|
|
69
|
+
return f"{origin_name}<{formatted_args}>"
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _format_stringified_annotation(annotation: Any) -> str:
|
|
74
|
+
"""Render fallback annotations from their string representation."""
|
|
75
|
+
rendered = str(annotation).replace("typing.", "").replace("'", "")
|
|
76
|
+
if rendered.endswith(" | None"):
|
|
77
|
+
return f"{rendered.removesuffix(' | None')} | null"
|
|
78
|
+
if "." in rendered and "[" not in rendered:
|
|
79
|
+
return rendered.rsplit(".", maxsplit=1)[-1]
|
|
80
|
+
return rendered
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _format_annotation(annotation: Any) -> str:
|
|
84
|
+
"""Render one Python type annotation in a TypeScript-like form.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
annotation: The Python annotation to render.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
A compact string suitable for prompt-facing function signatures.
|
|
91
|
+
"""
|
|
92
|
+
basic = _format_basic_annotation(annotation)
|
|
93
|
+
if basic is not None:
|
|
94
|
+
return basic
|
|
95
|
+
|
|
96
|
+
origin = get_origin(annotation)
|
|
97
|
+
if origin is None:
|
|
98
|
+
return _format_stringified_annotation(annotation)
|
|
99
|
+
|
|
100
|
+
args = get_args(annotation)
|
|
101
|
+
collection = _format_collection_annotation(origin, args)
|
|
102
|
+
if collection is not None:
|
|
103
|
+
return collection
|
|
104
|
+
|
|
105
|
+
generic = _format_generic_annotation(origin, args)
|
|
106
|
+
if generic is not None:
|
|
107
|
+
return generic
|
|
108
|
+
|
|
109
|
+
return _format_stringified_annotation(annotation)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _unwrap_typed_dict_annotation(annotation: Any) -> tuple[Any, str]:
|
|
113
|
+
"""Extract a TypedDict annotation from supported container types."""
|
|
114
|
+
container_prefix = ""
|
|
115
|
+
origin = get_origin(annotation)
|
|
116
|
+
if origin not in (list, tuple, set, frozenset):
|
|
117
|
+
return annotation, container_prefix
|
|
118
|
+
|
|
119
|
+
args = get_args(annotation)
|
|
120
|
+
if not args:
|
|
121
|
+
return annotation, container_prefix
|
|
122
|
+
|
|
123
|
+
unwrapped = args[0]
|
|
124
|
+
container_prefix = f"Contained `{unwrapped.__name__}` structure:\n"
|
|
125
|
+
return unwrapped, container_prefix
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _is_not_required_annotation(annotation: Any) -> bool:
|
|
129
|
+
"""Return whether a TypedDict field annotation marks an optional key."""
|
|
130
|
+
origin = get_origin(annotation)
|
|
131
|
+
if getattr(origin, "__name__", None) == "NotRequired":
|
|
132
|
+
return True
|
|
133
|
+
forward_arg = getattr(annotation, "__forward_arg__", None)
|
|
134
|
+
return isinstance(forward_arg, str) and forward_arg.startswith("NotRequired[")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _typed_dict_key_sets(
|
|
138
|
+
annotation: type[Any],
|
|
139
|
+
) -> tuple[frozenset[str], frozenset[str]]:
|
|
140
|
+
"""Return required and optional TypedDict keys, including postponed annotations."""
|
|
141
|
+
required_keys = getattr(annotation, "__required_keys__", frozenset())
|
|
142
|
+
optional_keys = getattr(annotation, "__optional_keys__", frozenset())
|
|
143
|
+
if optional_keys:
|
|
144
|
+
return required_keys, optional_keys
|
|
145
|
+
|
|
146
|
+
raw_annotations = getattr(annotation, "__annotations__", {})
|
|
147
|
+
inferred_optional = {
|
|
148
|
+
key
|
|
149
|
+
for key, value in raw_annotations.items()
|
|
150
|
+
if _is_not_required_annotation(value)
|
|
151
|
+
}
|
|
152
|
+
if not inferred_optional:
|
|
153
|
+
return required_keys, optional_keys
|
|
154
|
+
inferred_required = frozenset(set(raw_annotations) - inferred_optional)
|
|
155
|
+
return inferred_required, frozenset(inferred_optional)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _render_typed_dict_fields(
|
|
159
|
+
annotation: type[Any], field_types: dict[str, Any]
|
|
160
|
+
) -> str | None:
|
|
161
|
+
"""Render field lines for a TypedDict annotation."""
|
|
162
|
+
if not field_types:
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
required_keys, optional_keys = _typed_dict_key_sets(annotation)
|
|
166
|
+
lines = [f"Return structure `{annotation.__name__}`:"]
|
|
167
|
+
for key, value in field_types.items():
|
|
168
|
+
marker = "required" if key in required_keys else "optional"
|
|
169
|
+
if key not in required_keys and key not in optional_keys:
|
|
170
|
+
marker = "field"
|
|
171
|
+
field_name = f"{key}?" if key in optional_keys else key
|
|
172
|
+
lines.append(f"- {field_name}: {_format_annotation(value)} ({marker})")
|
|
173
|
+
return "\n".join(lines)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _format_typed_dict_structure(annotation: Any) -> str | None:
|
|
177
|
+
"""Render a compact field listing for a TypedDict annotation."""
|
|
178
|
+
annotation, container_prefix = _unwrap_typed_dict_annotation(annotation)
|
|
179
|
+
if not isinstance(annotation, type):
|
|
180
|
+
return None
|
|
181
|
+
if not hasattr(annotation, "__annotations__") or not hasattr(
|
|
182
|
+
annotation, "__required_keys__"
|
|
183
|
+
):
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
field_types = getattr(annotation, "__annotations__", {})
|
|
187
|
+
with contextlib.suppress(TypeError, NameError):
|
|
188
|
+
field_types = get_type_hints(annotation)
|
|
189
|
+
|
|
190
|
+
rendered_fields = _render_typed_dict_fields(annotation, field_types)
|
|
191
|
+
if rendered_fields is None:
|
|
192
|
+
return None
|
|
193
|
+
return container_prefix + rendered_fields
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def render_external_functions_section(
|
|
197
|
+
implementations: dict[str, Callable[..., Any] | BaseTool], *, add_docs: bool
|
|
198
|
+
) -> str:
|
|
199
|
+
"""Build the optional prompt section describing foreign functions."""
|
|
200
|
+
if not implementations:
|
|
201
|
+
return ""
|
|
202
|
+
|
|
203
|
+
if not add_docs:
|
|
204
|
+
formatted_functions = "\n".join(f"- {name}" for name in implementations)
|
|
205
|
+
return f"\n\nAvailable foreign functions:\n{formatted_functions}"
|
|
206
|
+
|
|
207
|
+
return f"\n\n{render_foreign_function_section(implementations)}"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _get_tool_doc_target(tool: BaseTool) -> Callable[..., Any] | None:
|
|
211
|
+
"""Choose the underlying callable that best represents a LangChain tool.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
tool: The LangChain tool being documented.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
The sync function or coroutine that provides the most useful signature
|
|
218
|
+
and docstring for prompt generation, or `None` if neither is available.
|
|
219
|
+
"""
|
|
220
|
+
target = getattr(tool, "func", None)
|
|
221
|
+
if callable(target):
|
|
222
|
+
return target
|
|
223
|
+
target = getattr(tool, "coroutine", None)
|
|
224
|
+
if callable(target):
|
|
225
|
+
return target
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _get_foreign_function_mode(implementation: Callable[..., Any] | BaseTool) -> str:
|
|
230
|
+
"""Return whether a foreign function should be treated as sync or async."""
|
|
231
|
+
if isinstance(implementation, BaseTool):
|
|
232
|
+
coroutine = getattr(implementation, "coroutine", None)
|
|
233
|
+
return "async" if callable(coroutine) else "sync"
|
|
234
|
+
return "async" if inspect.iscoroutinefunction(implementation) else "sync"
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _get_return_annotation(target: Callable[..., Any]) -> Any:
|
|
238
|
+
"""Resolve the return annotation for a callable, if present."""
|
|
239
|
+
with contextlib.suppress(TypeError, ValueError, NameError):
|
|
240
|
+
inspected_signature = inspect.signature(target)
|
|
241
|
+
resolved_hints = get_type_hints(target)
|
|
242
|
+
return resolved_hints.get("return", inspected_signature.return_annotation)
|
|
243
|
+
return inspect.Signature.empty
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _render_jsdoc(doc: str) -> str:
|
|
247
|
+
"""Convert a Python docstring into a compact JSDoc block."""
|
|
248
|
+
lines = inspect.cleandoc(doc).splitlines()
|
|
249
|
+
summary: list[str] = []
|
|
250
|
+
params: list[tuple[str, str]] = []
|
|
251
|
+
in_args = False
|
|
252
|
+
for line in lines:
|
|
253
|
+
stripped = line.strip()
|
|
254
|
+
if stripped == "Args:":
|
|
255
|
+
in_args = True
|
|
256
|
+
continue
|
|
257
|
+
if in_args:
|
|
258
|
+
if not stripped:
|
|
259
|
+
continue
|
|
260
|
+
if line.startswith(" ") and ":" in stripped:
|
|
261
|
+
name, description = stripped.split(":", maxsplit=1)
|
|
262
|
+
params.append((name.strip(), description.strip()))
|
|
263
|
+
continue
|
|
264
|
+
in_args = False
|
|
265
|
+
if stripped:
|
|
266
|
+
summary.append(stripped)
|
|
267
|
+
|
|
268
|
+
rendered = ["/**"]
|
|
269
|
+
rendered.extend(f" * {line}" for line in summary)
|
|
270
|
+
if summary and params:
|
|
271
|
+
rendered.append(" *")
|
|
272
|
+
for name, description in params:
|
|
273
|
+
rendered.append(f" * @param {name} {description}")
|
|
274
|
+
rendered.append(" */")
|
|
275
|
+
return "\n".join(rendered)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _render_function_stub(
|
|
279
|
+
name: str, implementation: Callable[..., Any] | BaseTool
|
|
280
|
+
) -> str:
|
|
281
|
+
"""Render one prompt-facing function declaration for a foreign function.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
name: The JavaScript-visible foreign function name.
|
|
285
|
+
implementation: The Python callable or LangChain tool backing that name.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
A TypeScript-like function declaration, optionally prefixed with a small
|
|
289
|
+
JSDoc block derived from the Python docstring.
|
|
290
|
+
"""
|
|
291
|
+
function_mode = _get_foreign_function_mode(implementation)
|
|
292
|
+
target = (
|
|
293
|
+
_get_tool_doc_target(implementation)
|
|
294
|
+
if isinstance(implementation, BaseTool)
|
|
295
|
+
else implementation
|
|
296
|
+
)
|
|
297
|
+
if target is None:
|
|
298
|
+
prefix = "async function" if function_mode == "async" else "function"
|
|
299
|
+
return f"{prefix} {name}(...args: any[]): any"
|
|
300
|
+
|
|
301
|
+
signature = "(...args: any[])"
|
|
302
|
+
return_annotation = inspect.Signature.empty
|
|
303
|
+
with contextlib.suppress(TypeError, ValueError, NameError):
|
|
304
|
+
inspected_signature = inspect.signature(target)
|
|
305
|
+
resolved_hints = get_type_hints(target)
|
|
306
|
+
parameter_parts = [
|
|
307
|
+
(
|
|
308
|
+
f"{param.name}: "
|
|
309
|
+
+ _format_annotation(resolved_hints.get(param.name, param.annotation))
|
|
310
|
+
)
|
|
311
|
+
if param.annotation is not inspect.Signature.empty
|
|
312
|
+
or param.name in resolved_hints
|
|
313
|
+
else f"{param.name}: any"
|
|
314
|
+
for param in inspected_signature.parameters.values()
|
|
315
|
+
]
|
|
316
|
+
signature = f"({', '.join(parameter_parts)})"
|
|
317
|
+
return_annotation = resolved_hints.get(
|
|
318
|
+
"return", inspected_signature.return_annotation
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
rendered_return = (
|
|
322
|
+
_format_annotation(return_annotation)
|
|
323
|
+
if return_annotation is not inspect.Signature.empty
|
|
324
|
+
else "any"
|
|
325
|
+
)
|
|
326
|
+
if function_mode == "async":
|
|
327
|
+
rendered_return = f"Promise<{rendered_return}>"
|
|
328
|
+
prefix = "async function" if function_mode == "async" else "function"
|
|
329
|
+
declaration = f"{prefix} {name}{signature}: {rendered_return}"
|
|
330
|
+
doc = inspect.getdoc(target) or inspect.getdoc(implementation)
|
|
331
|
+
if not doc:
|
|
332
|
+
return declaration
|
|
333
|
+
return f"{_render_jsdoc(doc)}\n{declaration}"
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _collect_referenced_types(
|
|
337
|
+
implementations: dict[str, Callable[..., Any] | BaseTool],
|
|
338
|
+
) -> list[type[Any]]:
|
|
339
|
+
"""Collect unique structured return types that should be documented.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
implementations: Mapping of JavaScript-visible function names to Python
|
|
343
|
+
callables or LangChain tools.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
A list of TypedDict-like annotations referenced by foreign function
|
|
347
|
+
return types, preserving first-seen order.
|
|
348
|
+
"""
|
|
349
|
+
collected: list[type[Any]] = []
|
|
350
|
+
seen: set[type[Any]] = set()
|
|
351
|
+
for implementation in implementations.values():
|
|
352
|
+
target = (
|
|
353
|
+
_get_tool_doc_target(implementation)
|
|
354
|
+
if isinstance(implementation, BaseTool)
|
|
355
|
+
else implementation
|
|
356
|
+
)
|
|
357
|
+
if target is None:
|
|
358
|
+
continue
|
|
359
|
+
annotation = _get_return_annotation(target)
|
|
360
|
+
origin = get_origin(annotation)
|
|
361
|
+
if origin in (list, tuple, set, frozenset):
|
|
362
|
+
args = get_args(annotation)
|
|
363
|
+
if args:
|
|
364
|
+
annotation = args[0]
|
|
365
|
+
if not isinstance(annotation, type):
|
|
366
|
+
continue
|
|
367
|
+
if not hasattr(annotation, "__annotations__") or not hasattr(
|
|
368
|
+
annotation, "__required_keys__"
|
|
369
|
+
):
|
|
370
|
+
continue
|
|
371
|
+
if annotation not in seen:
|
|
372
|
+
seen.add(annotation)
|
|
373
|
+
collected.append(annotation)
|
|
374
|
+
return collected
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _render_typed_dict_definition(annotation: type[Any]) -> str:
|
|
378
|
+
"""Render a TypeScript-like type definition for a TypedDict."""
|
|
379
|
+
_, optional_keys = _typed_dict_key_sets(annotation)
|
|
380
|
+
with contextlib.suppress(TypeError, NameError):
|
|
381
|
+
field_types = get_type_hints(annotation)
|
|
382
|
+
lines = [f"type {annotation.__name__} = {{"]
|
|
383
|
+
for key, value in field_types.items():
|
|
384
|
+
field_name = f"{key}?" if key in optional_keys else key
|
|
385
|
+
lines.append(f" {field_name}: {_format_annotation(value)}")
|
|
386
|
+
lines.append("}")
|
|
387
|
+
return "\n".join(lines)
|
|
388
|
+
|
|
389
|
+
field_types = getattr(annotation, "__annotations__", {})
|
|
390
|
+
lines = [f"type {annotation.__name__} = {{"]
|
|
391
|
+
for key, value in field_types.items():
|
|
392
|
+
field_name = f"{key}?" if key in optional_keys else key
|
|
393
|
+
lines.append(f" {field_name}: {_format_annotation(value)}")
|
|
394
|
+
lines.append("}")
|
|
395
|
+
return "\n".join(lines)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def render_foreign_function_section(
|
|
399
|
+
implementations: dict[str, Callable[..., Any] | BaseTool],
|
|
400
|
+
) -> str:
|
|
401
|
+
"""Render the complete prompt section for available foreign functions."""
|
|
402
|
+
function_blocks = [
|
|
403
|
+
_render_function_stub(name, implementation)
|
|
404
|
+
for name, implementation in implementations.items()
|
|
405
|
+
]
|
|
406
|
+
sections = [
|
|
407
|
+
"Available foreign functions:\n",
|
|
408
|
+
(
|
|
409
|
+
"These are JavaScript-callable foreign functions exposed inside QuickJS. "
|
|
410
|
+
"The TypeScript-style signatures below document argument and return shapes."
|
|
411
|
+
),
|
|
412
|
+
"",
|
|
413
|
+
"```ts",
|
|
414
|
+
"\n\n".join(function_blocks),
|
|
415
|
+
"```",
|
|
416
|
+
]
|
|
417
|
+
|
|
418
|
+
referenced_types = _collect_referenced_types(implementations)
|
|
419
|
+
if referenced_types:
|
|
420
|
+
type_blocks = [
|
|
421
|
+
_render_typed_dict_definition(annotation) for annotation in referenced_types
|
|
422
|
+
]
|
|
423
|
+
sections.extend(
|
|
424
|
+
[
|
|
425
|
+
"",
|
|
426
|
+
"Referenced types:",
|
|
427
|
+
"```ts",
|
|
428
|
+
"\n\n".join(type_blocks),
|
|
429
|
+
"```",
|
|
430
|
+
]
|
|
431
|
+
)
|
|
432
|
+
return "\n".join(sections)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def format_foreign_function_docs(
|
|
436
|
+
name: str,
|
|
437
|
+
implementation: Callable[..., Any] | BaseTool,
|
|
438
|
+
) -> str:
|
|
439
|
+
"""Render a compact signature and docstring block for a foreign function."""
|
|
440
|
+
return _render_function_stub(name, implementation)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
__all__ = [
|
|
444
|
+
"format_foreign_function_docs",
|
|
445
|
+
"render_external_functions_section",
|
|
446
|
+
"render_foreign_function_section",
|
|
447
|
+
]
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Bridge Python foreign functions into QuickJS with transparent JSON round-tripping.
|
|
2
|
+
|
|
3
|
+
The QuickJS Python binding can pass primitive return values directly, but complex
|
|
4
|
+
Python values like lists and dicts do not automatically become JavaScript arrays
|
|
5
|
+
or objects. This module adds a small bridge layer that JSON-encodes complex
|
|
6
|
+
Python results on the way out and parses them back inside QuickJS so foreign
|
|
7
|
+
functions behave more naturally from JavaScript.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import inspect
|
|
14
|
+
import json
|
|
15
|
+
import threading
|
|
16
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
17
|
+
|
|
18
|
+
from langchain_core.tools import BaseTool
|
|
19
|
+
from langchain_core.tools.base import (
|
|
20
|
+
_is_injected_arg_type,
|
|
21
|
+
get_all_basemodel_annotations,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Callable, Coroutine
|
|
26
|
+
from concurrent.futures import Future
|
|
27
|
+
|
|
28
|
+
import quickjs
|
|
29
|
+
from langchain.tools import ToolRuntime
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _AsyncLoopThread:
|
|
33
|
+
"""Run coroutines on a dedicated daemon-thread event loop."""
|
|
34
|
+
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
self._ready = threading.Event()
|
|
37
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
38
|
+
self._thread = threading.Thread(target=self._run, daemon=True)
|
|
39
|
+
self._thread.start()
|
|
40
|
+
self._ready.wait()
|
|
41
|
+
|
|
42
|
+
def _run(self) -> None:
|
|
43
|
+
loop = asyncio.new_event_loop()
|
|
44
|
+
asyncio.set_event_loop(loop)
|
|
45
|
+
self._loop = loop
|
|
46
|
+
self._ready.set()
|
|
47
|
+
loop.run_forever()
|
|
48
|
+
|
|
49
|
+
def submit(self, coroutine: Coroutine[Any, Any, Any]) -> Future[Any]:
|
|
50
|
+
loop = self._loop
|
|
51
|
+
if loop is None:
|
|
52
|
+
msg = "Async loop thread was not initialized."
|
|
53
|
+
raise RuntimeError(msg)
|
|
54
|
+
return asyncio.run_coroutine_threadsafe(coroutine, loop)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
_ASYNC_LOOP_THREAD = _AsyncLoopThread()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _await_if_needed(value: Any) -> Any:
|
|
61
|
+
"""Resolve awaitable results on the background event loop when needed."""
|
|
62
|
+
if inspect.isawaitable(value):
|
|
63
|
+
return _ASYNC_LOOP_THREAD.submit(value).result()
|
|
64
|
+
return value
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _invoke_tool(
|
|
68
|
+
tool: BaseTool,
|
|
69
|
+
payload: str | dict[str, Any],
|
|
70
|
+
*,
|
|
71
|
+
prefer_async: bool = False,
|
|
72
|
+
) -> Any:
|
|
73
|
+
"""Invoke a tool through its sync or async entrypoint as appropriate."""
|
|
74
|
+
has_async = getattr(tool, "coroutine", None) is not None or (
|
|
75
|
+
tool.__class__._arun is not BaseTool._arun # noqa: SLF001
|
|
76
|
+
)
|
|
77
|
+
has_sync = getattr(tool, "func", None) is not None or (
|
|
78
|
+
tool.__class__._run is not BaseTool._run # noqa: SLF001
|
|
79
|
+
)
|
|
80
|
+
if has_async and (prefer_async or not has_sync):
|
|
81
|
+
return _await_if_needed(tool.ainvoke(payload))
|
|
82
|
+
return _await_if_needed(tool.invoke(payload))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_ptc_implementations(
|
|
86
|
+
ptc: list[Callable[..., Any] | BaseTool] | None,
|
|
87
|
+
) -> dict[str, Callable[..., Any] | BaseTool]:
|
|
88
|
+
"""Return configured PTC implementations keyed by exported function name."""
|
|
89
|
+
implementations: dict[str, Callable[..., Any] | BaseTool] = {}
|
|
90
|
+
for implementation in ptc or []:
|
|
91
|
+
if isinstance(implementation, BaseTool):
|
|
92
|
+
implementations[implementation.name] = implementation
|
|
93
|
+
continue
|
|
94
|
+
name = getattr(implementation, "__name__", None)
|
|
95
|
+
if isinstance(name, str):
|
|
96
|
+
implementations[name] = implementation
|
|
97
|
+
return implementations
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _get_runtime_arg_name(tool: BaseTool) -> str | None:
|
|
101
|
+
"""Return the injected runtime parameter name for a tool, if any."""
|
|
102
|
+
for name, type_ in get_all_basemodel_annotations(tool.get_input_schema()).items():
|
|
103
|
+
if name == "runtime" and _is_injected_arg_type(type_):
|
|
104
|
+
return name
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _build_tool_payload(
|
|
109
|
+
tool: BaseTool,
|
|
110
|
+
args: tuple[Any, ...],
|
|
111
|
+
kwargs: dict[str, Any],
|
|
112
|
+
*,
|
|
113
|
+
runtime: ToolRuntime | None = None,
|
|
114
|
+
) -> str | dict[str, Any]:
|
|
115
|
+
"""Convert QuickJS call arguments into a LangChain tool payload."""
|
|
116
|
+
input_schema = tool.get_input_schema()
|
|
117
|
+
schema_annotations = getattr(input_schema, "__annotations__", {})
|
|
118
|
+
fields = [
|
|
119
|
+
name
|
|
120
|
+
for name, type_ in schema_annotations.items()
|
|
121
|
+
if not _is_injected_arg_type(type_)
|
|
122
|
+
]
|
|
123
|
+
runtime_arg_name = _get_runtime_arg_name(tool)
|
|
124
|
+
|
|
125
|
+
if kwargs:
|
|
126
|
+
payload: str | dict[str, Any] = kwargs
|
|
127
|
+
elif (
|
|
128
|
+
len(args) == 1 and isinstance(args[0], (str, dict)) and runtime_arg_name is None
|
|
129
|
+
):
|
|
130
|
+
payload = args[0]
|
|
131
|
+
elif len(args) == 1 and len(fields) == 1:
|
|
132
|
+
payload = {fields[0]: args[0]}
|
|
133
|
+
elif len(args) == len(fields) and fields:
|
|
134
|
+
payload = dict(zip(fields, args, strict=False))
|
|
135
|
+
else:
|
|
136
|
+
payload = {"args": list(args)}
|
|
137
|
+
|
|
138
|
+
if (
|
|
139
|
+
runtime is not None
|
|
140
|
+
and runtime_arg_name is not None
|
|
141
|
+
and isinstance(payload, dict)
|
|
142
|
+
):
|
|
143
|
+
return {**payload, runtime_arg_name: runtime}
|
|
144
|
+
return payload
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _wrap_tool_for_js(
|
|
148
|
+
tool: BaseTool,
|
|
149
|
+
*,
|
|
150
|
+
prefer_async: bool = False,
|
|
151
|
+
runtime: ToolRuntime | None = None,
|
|
152
|
+
) -> Callable[..., Any]:
|
|
153
|
+
"""Adapt a LangChain tool into a plain sync callable for QuickJS."""
|
|
154
|
+
|
|
155
|
+
def tool_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
156
|
+
payload = _build_tool_payload(tool, args, kwargs, runtime=runtime)
|
|
157
|
+
return _invoke_tool(tool, payload, prefer_async=prefer_async)
|
|
158
|
+
|
|
159
|
+
return tool_wrapper
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _serialize_for_js(value: Any) -> Any:
|
|
163
|
+
"""Convert Python return values into primitives the bridge can round-trip."""
|
|
164
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
165
|
+
return value
|
|
166
|
+
return json.dumps(value)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _wrap_function_for_js(implementation: Callable[..., Any]) -> Callable[..., Any]:
|
|
170
|
+
"""Wrap a Python callable so complex return values are JSON-encoded."""
|
|
171
|
+
|
|
172
|
+
def function_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
173
|
+
return _serialize_for_js(_await_if_needed(implementation(*args, **kwargs)))
|
|
174
|
+
|
|
175
|
+
return function_wrapper
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _raw_function_name(name: str) -> str:
|
|
179
|
+
"""Build the hidden Python callable name used by the JS bridge shim."""
|
|
180
|
+
return f"__python_{name}"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _build_external_functions(
|
|
184
|
+
implementations: dict[str, Callable[..., Any] | BaseTool] | None,
|
|
185
|
+
*,
|
|
186
|
+
prefer_async: bool = False,
|
|
187
|
+
runtime: ToolRuntime | None = None,
|
|
188
|
+
) -> dict[str, Callable[..., Any]]:
|
|
189
|
+
"""Normalize foreign implementations into QuickJS-registerable callables."""
|
|
190
|
+
external_functions: dict[str, Callable[..., Any]] = {}
|
|
191
|
+
for name, implementation in (implementations or {}).items():
|
|
192
|
+
callable_implementation = (
|
|
193
|
+
_wrap_tool_for_js(
|
|
194
|
+
implementation,
|
|
195
|
+
prefer_async=prefer_async,
|
|
196
|
+
runtime=runtime,
|
|
197
|
+
)
|
|
198
|
+
if isinstance(implementation, BaseTool)
|
|
199
|
+
else implementation
|
|
200
|
+
)
|
|
201
|
+
external_functions[_raw_function_name(name)] = _wrap_function_for_js(
|
|
202
|
+
callable_implementation
|
|
203
|
+
)
|
|
204
|
+
return external_functions
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
_EXTERNAL_FUNCTION_SHIM_TEMPLATE = """
|
|
208
|
+
globalThis[{name}] = (...args) => {{
|
|
209
|
+
const value = globalThis[{raw_name}](...args);
|
|
210
|
+
if (typeof value !== \"string\") {{ return value; }}
|
|
211
|
+
const trimmed = value.trim();
|
|
212
|
+
if (!trimmed) {{ return value; }}
|
|
213
|
+
const first = trimmed[0];
|
|
214
|
+
if (first !== \"[\" && first !== \"{{\") {{ return value; }}
|
|
215
|
+
return JSON.parse(value);
|
|
216
|
+
}};
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def inject_external_function_shims(
|
|
221
|
+
context: quickjs.Context, external_functions: list[str] | None
|
|
222
|
+
) -> None:
|
|
223
|
+
"""Install JavaScript shims for foreign functions inside a QuickJS context."""
|
|
224
|
+
if not external_functions:
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
shim_lines = []
|
|
228
|
+
for name in external_functions:
|
|
229
|
+
raw_name = _raw_function_name(name)
|
|
230
|
+
shim_lines.append(
|
|
231
|
+
_EXTERNAL_FUNCTION_SHIM_TEMPLATE.format(
|
|
232
|
+
name=json.dumps(name),
|
|
233
|
+
raw_name=json.dumps(raw_name),
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
context.eval("".join(shim_lines))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def install_external_functions(
|
|
240
|
+
context: quickjs.Context,
|
|
241
|
+
implementations: dict[str, Callable[..., Any] | BaseTool] | None,
|
|
242
|
+
*,
|
|
243
|
+
execution_mode: Literal["sync", "async"] = "sync",
|
|
244
|
+
runtime: ToolRuntime | None = None,
|
|
245
|
+
) -> None:
|
|
246
|
+
"""Install foreign functions and JavaScript shims into a QuickJS context."""
|
|
247
|
+
external_functions = _build_external_functions(
|
|
248
|
+
implementations,
|
|
249
|
+
prefer_async=execution_mode == "async",
|
|
250
|
+
runtime=runtime,
|
|
251
|
+
)
|
|
252
|
+
for name, implementation in external_functions.items():
|
|
253
|
+
context.add_callable(name, implementation)
|
|
254
|
+
inject_external_function_shims(context, list(implementations or {}))
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
__all__ = ["get_ptc_implementations", "install_external_functions"]
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Middleware for providing a QuickJS-backed repl tool to an agent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Annotated, Any
|
|
6
|
+
|
|
7
|
+
import quickjs
|
|
8
|
+
from deepagents.middleware._utils import append_to_system_message
|
|
9
|
+
from langchain.agents.middleware.types import (
|
|
10
|
+
AgentMiddleware,
|
|
11
|
+
AgentState,
|
|
12
|
+
ContextT,
|
|
13
|
+
ModelRequest,
|
|
14
|
+
ModelResponse,
|
|
15
|
+
ResponseT,
|
|
16
|
+
)
|
|
17
|
+
from langchain.tools import ToolRuntime
|
|
18
|
+
from langchain_core.tools import BaseTool, StructuredTool
|
|
19
|
+
|
|
20
|
+
from langchain_quickjs._foreign_function_docs import render_external_functions_section
|
|
21
|
+
from langchain_quickjs._foreign_functions import (
|
|
22
|
+
get_ptc_implementations,
|
|
23
|
+
install_external_functions,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from collections.abc import Awaitable, Callable
|
|
28
|
+
|
|
29
|
+
from langchain.tools import ToolRuntime
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
REPL_TOOL_DESCRIPTION = """Evaluates code using a QuickJS-backed JavaScript REPL.
|
|
33
|
+
|
|
34
|
+
CRITICAL: The REPL does NOT retain state between calls. Each `repl` invocation is evaluated from scratch.
|
|
35
|
+
Do NOT assume variables, functions, imports, or helper objects from prior `repl` calls are available.
|
|
36
|
+
|
|
37
|
+
Capabilities and limitations:
|
|
38
|
+
- Executes JavaScript with QuickJS.
|
|
39
|
+
- Use `print(...)` to emit output. The tool returns printed lines joined with newlines.
|
|
40
|
+
- The final expression value is returned only if nothing was printed.
|
|
41
|
+
- There is no filesystem or network access unless you expose Python callables as foreign functions.
|
|
42
|
+
{external_functions_section}
|
|
43
|
+
""" # noqa: E501 # preserve prompt text formatting exactly for the model
|
|
44
|
+
|
|
45
|
+
REPL_SYSTEM_PROMPT = """## REPL tool
|
|
46
|
+
|
|
47
|
+
You have access to a `repl` tool.
|
|
48
|
+
|
|
49
|
+
CRITICAL: The REPL does NOT retain state between calls. Each `repl` invocation is evaluated from scratch.
|
|
50
|
+
Do NOT assume variables, functions, imports, or helper objects from prior `repl` calls are available.
|
|
51
|
+
|
|
52
|
+
- The REPL executes JavaScript with QuickJS.
|
|
53
|
+
- Use `print(...)` to emit output. The tool returns printed lines joined with newlines.
|
|
54
|
+
- The final expression value is returned only if nothing was printed.
|
|
55
|
+
- There is no filesystem or network access unless equivalent foreign functions have been provided.
|
|
56
|
+
- Use it for small computations, control flow, JSON manipulation, and calling externally registered foreign functions.
|
|
57
|
+
{external_functions_section}
|
|
58
|
+
""" # noqa: E501 # preserve prompt text formatting exactly for the model
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class QuickJSMiddleware(AgentMiddleware[AgentState[Any], ContextT, ResponseT]):
|
|
62
|
+
"""Provide a QuickJS-backed `repl` tool to an agent."""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
*,
|
|
67
|
+
ptc: list[Callable[..., Any] | BaseTool] | None = None,
|
|
68
|
+
add_ptc_docs: bool = False,
|
|
69
|
+
timeout: int | None = None,
|
|
70
|
+
memory_limit: int | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Initialize the middleware.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
ptc: Functions or LangChain tools to expose inside the REPL.
|
|
76
|
+
add_ptc_docs: Whether to add signatures and docstrings for exposed PTC
|
|
77
|
+
functions to the system prompt.
|
|
78
|
+
timeout: Optional timeout in seconds for each evaluation.
|
|
79
|
+
memory_limit: Optional memory limit in bytes for each evaluation.
|
|
80
|
+
"""
|
|
81
|
+
self._ptc = ptc or []
|
|
82
|
+
self._add_ptc_docs = add_ptc_docs
|
|
83
|
+
self._timeout = timeout
|
|
84
|
+
self._memory_limit = memory_limit
|
|
85
|
+
self.tools = [self._create_repl_tool()]
|
|
86
|
+
|
|
87
|
+
def _format_repl_system_prompt(self) -> str:
|
|
88
|
+
"""Build the system prompt fragment describing the repl tool."""
|
|
89
|
+
external_functions_section = render_external_functions_section(
|
|
90
|
+
get_ptc_implementations(self._ptc),
|
|
91
|
+
add_docs=self._add_ptc_docs,
|
|
92
|
+
)
|
|
93
|
+
return REPL_SYSTEM_PROMPT.format(
|
|
94
|
+
external_functions_section=external_functions_section
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def modify_request(self, request: ModelRequest[ContextT]) -> ModelRequest[ContextT]:
|
|
98
|
+
"""Inject REPL usage instructions into the system message."""
|
|
99
|
+
repl_prompt = self._format_repl_system_prompt()
|
|
100
|
+
new_system_message = append_to_system_message(
|
|
101
|
+
request.system_message, repl_prompt
|
|
102
|
+
)
|
|
103
|
+
return request.override(system_message=new_system_message)
|
|
104
|
+
|
|
105
|
+
def wrap_model_call(
|
|
106
|
+
self,
|
|
107
|
+
request: ModelRequest[ContextT],
|
|
108
|
+
handler: Callable[[ModelRequest[ContextT]], ModelResponse[ResponseT]],
|
|
109
|
+
) -> ModelResponse[ResponseT]:
|
|
110
|
+
"""Wrap model call to inject REPL instructions into system prompt."""
|
|
111
|
+
modified_request = self.modify_request(request)
|
|
112
|
+
return handler(modified_request)
|
|
113
|
+
|
|
114
|
+
async def awrap_model_call(
|
|
115
|
+
self,
|
|
116
|
+
request: ModelRequest[ContextT],
|
|
117
|
+
handler: Callable[
|
|
118
|
+
[ModelRequest[ContextT]], Awaitable[ModelResponse[ResponseT]]
|
|
119
|
+
],
|
|
120
|
+
) -> ModelResponse[ResponseT]:
|
|
121
|
+
"""Async wrap model call to inject REPL instructions into system prompt."""
|
|
122
|
+
modified_request = self.modify_request(request)
|
|
123
|
+
return await handler(modified_request)
|
|
124
|
+
|
|
125
|
+
def _create_context(
|
|
126
|
+
self,
|
|
127
|
+
timeout: int | None,
|
|
128
|
+
printed_lines: list[str],
|
|
129
|
+
*,
|
|
130
|
+
prefer_async: bool = False,
|
|
131
|
+
runtime: ToolRuntime | None = None,
|
|
132
|
+
) -> quickjs.Context:
|
|
133
|
+
"""Create a configured QuickJS context for a single evaluation."""
|
|
134
|
+
context = quickjs.Context()
|
|
135
|
+
effective_timeout = timeout if timeout is not None else self._timeout
|
|
136
|
+
if effective_timeout is not None:
|
|
137
|
+
if effective_timeout <= 0:
|
|
138
|
+
msg = f"timeout must be positive, got {effective_timeout}."
|
|
139
|
+
raise ValueError(msg)
|
|
140
|
+
context.set_time_limit(effective_timeout)
|
|
141
|
+
if self._memory_limit is not None:
|
|
142
|
+
if self._memory_limit <= 0:
|
|
143
|
+
msg = f"memory_limit must be positive, got {self._memory_limit}."
|
|
144
|
+
raise ValueError(msg)
|
|
145
|
+
context.set_memory_limit(self._memory_limit)
|
|
146
|
+
|
|
147
|
+
def _print(*args: Any) -> None:
|
|
148
|
+
printed_lines.append(" ".join(map(str, args)))
|
|
149
|
+
|
|
150
|
+
context.add_callable("print", _print)
|
|
151
|
+
install_external_functions(
|
|
152
|
+
context,
|
|
153
|
+
get_ptc_implementations(self._ptc),
|
|
154
|
+
execution_mode="async" if prefer_async else "sync",
|
|
155
|
+
runtime=runtime,
|
|
156
|
+
)
|
|
157
|
+
return context
|
|
158
|
+
|
|
159
|
+
def _evaluate(
|
|
160
|
+
self,
|
|
161
|
+
code: str,
|
|
162
|
+
*,
|
|
163
|
+
timeout: int | None,
|
|
164
|
+
prefer_async: bool = False,
|
|
165
|
+
runtime: ToolRuntime | None = None,
|
|
166
|
+
) -> str:
|
|
167
|
+
"""Execute JavaScript and return printed output or final value."""
|
|
168
|
+
printed_lines: list[str] = []
|
|
169
|
+
try:
|
|
170
|
+
context = self._create_context(
|
|
171
|
+
timeout,
|
|
172
|
+
printed_lines,
|
|
173
|
+
prefer_async=prefer_async,
|
|
174
|
+
runtime=runtime,
|
|
175
|
+
)
|
|
176
|
+
except ValueError as exc:
|
|
177
|
+
return f"Error: {exc}"
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
value = context.eval(code)
|
|
181
|
+
except quickjs.JSException as exc:
|
|
182
|
+
return str(exc)
|
|
183
|
+
|
|
184
|
+
if printed_lines:
|
|
185
|
+
return "\n".join(printed_lines).rstrip()
|
|
186
|
+
if value is None:
|
|
187
|
+
return ""
|
|
188
|
+
return str(value)
|
|
189
|
+
|
|
190
|
+
def _create_repl_tool(self) -> BaseTool:
|
|
191
|
+
"""Create the LangChain tool wrapper around QuickJS execution."""
|
|
192
|
+
|
|
193
|
+
def _sync_quickjs(
|
|
194
|
+
code: Annotated[str, "Code string to evaluate in QuickJS."],
|
|
195
|
+
runtime: ToolRuntime,
|
|
196
|
+
timeout: Annotated[
|
|
197
|
+
int | None, "Optional timeout in seconds for this evaluation."
|
|
198
|
+
] = None,
|
|
199
|
+
) -> str:
|
|
200
|
+
"""Execute a single QuickJS program and return captured stdout."""
|
|
201
|
+
return self._evaluate(
|
|
202
|
+
code,
|
|
203
|
+
timeout=timeout,
|
|
204
|
+
prefer_async=False,
|
|
205
|
+
runtime=runtime,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
async def _async_quickjs(
|
|
209
|
+
code: Annotated[str, "Code string to evaluate in QuickJS."],
|
|
210
|
+
runtime: ToolRuntime,
|
|
211
|
+
timeout: Annotated[
|
|
212
|
+
int | None, "Optional timeout in seconds for this evaluation."
|
|
213
|
+
] = None,
|
|
214
|
+
) -> str:
|
|
215
|
+
"""Execute a single QuickJS program in the async tool path."""
|
|
216
|
+
return self._evaluate(
|
|
217
|
+
code,
|
|
218
|
+
timeout=timeout,
|
|
219
|
+
prefer_async=True,
|
|
220
|
+
runtime=runtime,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
tool_description = REPL_TOOL_DESCRIPTION.format(
|
|
224
|
+
external_functions_section=render_external_functions_section(
|
|
225
|
+
get_ptc_implementations(self._ptc),
|
|
226
|
+
add_docs=self._add_ptc_docs,
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
return StructuredTool.from_function(
|
|
231
|
+
name="repl",
|
|
232
|
+
description=tool_description,
|
|
233
|
+
func=_sync_quickjs,
|
|
234
|
+
coroutine=_async_quickjs,
|
|
235
|
+
)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langchain-quickjs
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: QuickJS integration package for Deep Agents
|
|
5
|
+
Project-URL: Homepage, https://github.com/langchain-ai/deepagents/tree/main/libs/partners/quickjs
|
|
6
|
+
Project-URL: Repository, https://github.com/langchain-ai/deepagents
|
|
7
|
+
Project-URL: Documentation, https://github.com/langchain-ai/deepagents/tree/main/libs/partners/quickjs
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Requires-Python: <4.0,>=3.11
|
|
19
|
+
Requires-Dist: deepagents
|
|
20
|
+
Requires-Dist: quickjs<2,>=1.19.4
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# langchain-quickjs
|
|
24
|
+
|
|
25
|
+
`langchain-quickjs` provides a QuickJS-backed REPL middleware for Deep Agents. It adds a `repl` tool that can evaluate small JavaScript snippets for computation, control flow, JSON manipulation, and calls to exposed Python foreign functions.
|
|
26
|
+
|
|
27
|
+
## Basic usage
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from deepagents import create_deep_agent
|
|
31
|
+
from langchain_quickjs import QuickJSMiddleware
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def normalize_name(name: str) -> str:
|
|
35
|
+
return name.strip().lower()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
agent = create_deep_agent(
|
|
39
|
+
model="openai:gpt-4.1",
|
|
40
|
+
tools=[],
|
|
41
|
+
middleware=[
|
|
42
|
+
QuickJSMiddleware(
|
|
43
|
+
ptc=[normalize_name],
|
|
44
|
+
add_ptc_docs=True,
|
|
45
|
+
)
|
|
46
|
+
],
|
|
47
|
+
)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
With this middleware installed, the agent receives a `repl` tool that runs each JavaScript evaluation in a fresh QuickJS context. If you expose Python callables through `ptc`, they are available inside the REPL as foreign functions.
|
|
51
|
+
|
|
52
|
+
## REPL behavior
|
|
53
|
+
|
|
54
|
+
- The REPL is stateless. Each call starts from a fresh QuickJS context, so variables, functions, and other values defined in one `repl` call are not available in the next one.
|
|
55
|
+
- Execution uses [QuickJS](https://bellard.org/quickjs/), so JavaScript support is limited to what QuickJS provides. It is good for small computations, control flow, JSON manipulation, and calling exposed foreign functions, but it is not a browser or Node.js runtime and does not provide their APIs.
|
|
56
|
+
- Foreign functions support passing primitive values between JavaScript and Python, including `int`, `float`, `bool`, `str`, and `None`. Lists and dictionaries returned from Python are also supported and are bridged back into JavaScript arrays and objects.
|
|
57
|
+
- Async foreign functions are supported. Because QuickJS callbacks are synchronous, awaitables are delegated to a dedicated daemon-thread event loop and their resolved results are returned back into the REPL call.
|
|
58
|
+
|
|
59
|
+
## Current limitations
|
|
60
|
+
|
|
61
|
+
- Does not work with HIL in the REPL.
|
|
62
|
+
- Does not support `ToolRuntime` yet.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
langchain_quickjs/__init__.py,sha256=XJLajb7cQ2pYcFL2B5W_QqLJ47glRvge2SF9Z4e3IxQ,182
|
|
2
|
+
langchain_quickjs/_foreign_function_docs.py,sha256=2mkq4dUhXDuGKOHL5NArcbc7HSI6uzmH0ZMinaHUxeM,16049
|
|
3
|
+
langchain_quickjs/_foreign_functions.py,sha256=qwK0XTpTwb6SWXZ-Wx4groSnZXVnwV6ez3pOaOwZxdc,8506
|
|
4
|
+
langchain_quickjs/middleware.py,sha256=54SXMF2Ojw5pXLLz7UtPSie87PtIYOYKChAiy8--4hY,8717
|
|
5
|
+
langchain_quickjs-0.0.1.dist-info/METADATA,sha256=0WMdwzpjZIZl_7O-NNxZoECEObNoZ8Es1S9IUv5SZbs,2921
|
|
6
|
+
langchain_quickjs-0.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
langchain_quickjs-0.0.1.dist-info/licenses/LICENSE,sha256=TsZ-TKbmch26hJssqCJhWXyGph7iFLvyFBYAa3stBHg,1067
|
|
8
|
+
langchain_quickjs-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) LangChain, 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.
|