File size: 26,420 Bytes
08b963f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
"""Restricted stateful Python for Nemotron Cascade-2 math tool calls.

Tercet-R saw `stateful_python_code_exec` on `math/math_tool.jsonl` during
the Cascade-2 SFT stage. Observations are the Jupyter-style stdout / last
expression value, not a JSON envelope.

The worker process only allows a math-oriented import whitelist and rejects
dunder access, so a tool call cannot read the filesystem or start a shell.
"""

from __future__ import annotations

import ast
import io
import itertools
import json
import math
import os
import re
import statistics
import subprocess
import sys
import threading
from collections.abc import Iterable, Iterator, Mapping
from pathlib import Path
from queue import Empty, Queue
from typing import Any


NEMOTRON_PYTHON_EXEC_TOOL: dict[str, Any] = {
    "type": "function",
    "function": {
        "name": "stateful_python_code_exec",
        "description": (
            "Call this function to execute Python code in a stateful Jupyter "
            "notebook environment. Python will respond with the output of the "
            "execution or time out after 120.0 seconds."
        ),
        "parameters": {
            "type": "object",
            "properties": {
                "code": {
                    "type": "string",
                    "description": "Code to execute",
                }
            },
            "required": ["code"],
        },
    },
}

CALCULATOR_TOOL: dict[str, Any] = {
    "type": "function",
    "function": {
        "name": "calculator",
        "description": "Evaluate a math expression and return the result.",
        "parameters": {
            "type": "object",
            "properties": {
                "expression": {
                    "type": "string",
                    "description": (
                        "A math expression in standard calculator notation, for example "
                        "(3 + 4)^2 * sqrt(2) / ln(10). Use + - * / ^ (power), % (remainder), "
                        "! (factorial) and parentheses; the constants pi and e; and functions "
                        "such as sqrt, cbrt, exp, ln, log (natural, or log(x, base)), log10, "
                        "log2, sin, cos, tan, asin, acos, atan (radians), abs, round, floor, "
                        "ceil, factorial, comb, perm, gcd, lcm, min, max, sum, mean and median."
                    ),
                }
            },
            "required": ["expression"],
        },
    },
}

PYTHON_EXEC_TOOL_NAMES = frozenset(
    {
        "stateful_python_code_exec",
        "python_code_exec",
        "code_interpreter",
        "python_exec",
        "python",
    }
)
CALCULATOR_TOOL_NAMES = frozenset({"calculator", "calc"})
DEFAULT_EXEC_TIMEOUT_SECONDS = 30.0
_SRC_ROOT = Path(__file__).resolve().parents[1]

_session_lock = threading.Lock()
_default_session: StatefulPythonSession | None = None


def _normalize_tool_name(name: str) -> str:
    return name.strip().lower().replace("-", "_")


def is_python_exec_tool_name(name: str) -> bool:
    return _normalize_tool_name(name) in PYTHON_EXEC_TOOL_NAMES


def is_calculator_tool_name(name: str) -> bool:
    return _normalize_tool_name(name) in CALCULATOR_TOOL_NAMES


def is_auto_math_tool_name(name: str) -> bool:
    return is_python_exec_tool_name(name) or is_calculator_tool_name(name)


def is_calculator_expression(source: str) -> bool:
    """A one-line calculator call runs in the calculator; several statements run as notebook code."""
    return "\n" not in source.strip() and ";" not in source


def code_from_arguments(arguments: Mapping[str, Any] | None) -> str:
    if not arguments:
        return ""
    for key in ("code", "expression", "expr", "source"):
        raw = arguments.get(key)
        if isinstance(raw, str) and raw.strip():
            return raw
        if raw is not None and key != "code":
            return str(raw)
    return ""


class CodeExecError(ValueError):
    """Rejected or failed tool code."""


class CalculatorError(CodeExecError):
    """An expression the calculator cannot evaluate. The message says how to fix it."""


class StatefulPythonSession:
    """One long-lived restricted interpreter, matching the training tool."""

    def __init__(self, *, timeout_seconds: float = DEFAULT_EXEC_TIMEOUT_SECONDS) -> None:
        self.timeout_seconds = timeout_seconds
        self._lock = threading.Lock()
        self._process: subprocess.Popen[str] | None = None

    def close(self) -> None:
        with self._lock:
            self._kill_locked()

    def reset(self) -> None:
        with self._lock:
            self._kill_locked()

    def run(self, code: str) -> str:
        text = code.strip()
        if not text:
            raise CodeExecError("code is empty")
        with self._lock:
            return self._run_locked(text)

    def _run_locked(self, code: str) -> str:
        process = self._ensure_process_locked()
        try:
            process.stdin.write(json.dumps({"code": code}, ensure_ascii=False) + "\n")
            process.stdin.flush()
            line = self._readline_locked(process, self.timeout_seconds)
        except (BrokenPipeError, OSError) as error:
            self._kill_locked()
            raise CodeExecError(f"python worker died: {error}") from error
        if not line:
            stderr = ""
            if process.stderr is not None:
                try:
                    stderr = process.stderr.read()
                except OSError:
                    stderr = ""
            self._kill_locked()
            detail = stderr.strip() or "python worker closed stdout"
            raise CodeExecError(detail)
        try:
            payload = json.loads(line)
        except json.JSONDecodeError as error:
            self._kill_locked()
            raise CodeExecError(f"python worker returned invalid JSON: {line!r}") from error
        if not isinstance(payload, dict):
            raise CodeExecError("python worker returned a non-object")
        output = str(payload.get("output") or "")
        if payload.get("ok") is True:
            return output if output else "None"
        raise CodeExecError(output or "execution failed")

    def _readline_locked(self, process: subprocess.Popen[str], timeout: float) -> str:
        if process.stdout is None:
            raise CodeExecError("python worker has no stdout")
        lines: Queue[str] = Queue()

        def _read() -> None:
            lines.put(process.stdout.readline() if process.stdout is not None else "")

        reader = threading.Thread(target=_read, daemon=True)
        reader.start()
        reader.join(timeout)
        if reader.is_alive():
            self._kill_locked()
            raise CodeExecError(f"timed out after {timeout:g}s")
        try:
            return lines.get_nowait()
        except Empty:
            return ""

    def _ensure_process_locked(self) -> subprocess.Popen[str]:
        process = self._process
        if process is not None and process.poll() is None:
            return process
        env = os.environ.copy()
        pythonpath = env.get("PYTHONPATH", "")
        env["PYTHONPATH"] = (
            str(_SRC_ROOT) if not pythonpath else f"{_SRC_ROOT}{os.pathsep}{pythonpath}"
        )
        self._process = subprocess.Popen(
            [sys.executable, "-m", "tiny_gdn.code_exec"],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            bufsize=1,
            env=env,
        )
        if self._process.stdin is None or self._process.stdout is None:
            self._kill_locked()
            raise CodeExecError("failed to start python worker")
        return self._process

    def _kill_locked(self) -> None:
        process = self._process
        self._process = None
        if process is None:
            return
        try:
            process.kill()
            process.wait(timeout=2)
        except (OSError, subprocess.TimeoutExpired):
            pass


def default_python_session() -> StatefulPythonSession:
    global _default_session
    with _session_lock:
        if _default_session is None:
            _default_session = StatefulPythonSession()
        return _default_session


def reset_default_python_session() -> None:
    global _default_session
    with _session_lock:
        session = _default_session
        _default_session = None
    if session is not None:
        session.close()


MAX_EXPRESSION_CHARS = 10_000
# Exact integer powers past this many bits are computed as floats, and overflow there.
_MAX_EXACT_POWER_BITS = 100_000
_MAX_FACTORIAL = 100_000
_MAX_LISTED_ITEMS = 1_000
# About 3,600 digits, inside the 4,300 that Python's int-to-str conversion allows.
_MAX_EXACT_INTEGER_BITS = 12_000
_POWER = "_calculator_power"
_SYMBOLS = str.maketrans(
    {"×": "*", "·": "*", "÷": "/", "−": "-", "–": "-", "π": "pi", "∞": "inf", "²": "**2", "³": "**3"}
)


def calculate(expression: str, variables: Mapping[str, Any] | None = None) -> str:
    """Evaluate one expression in calculator notation and format its result.

    `^` is a power and `n!` a factorial, as on a calculator, and functions
    such as sqrt, ln and sin need no `math.` prefix. Python's `**`, `math.`
    names and comprehensions work as well. `variables` are names that a
    notebook in the same conversation defined.
    """
    text = _calculator_text(expression)
    try:
        tree = ast.parse(text, mode="eval")
    except SyntaxError as error:
        raise CalculatorError(
            f"could not read the expression ({error.msg}). "
            "Write every operator out, for example 2*pi or 3*(4 + 5)."
        ) from None
    _reject_unsafe_ast(tree)
    tree = ast.fix_missing_locations(_GuardedPower().visit(tree))
    # One dict as globals, so generator expressions see the functions too.
    namespace: dict[str, Any] = {"__builtins__": {}, **_calculator_functions(), **(variables or {}), _POWER: _power}
    try:
        value = eval(compile(tree, "<calculator>", "eval"), namespace)  # noqa: S307 — AST already rejected unsafe nodes
        return _calculator_result(value)
    except CodeExecError:
        raise
    except NameError as error:
        raise CalculatorError(
            f"unknown name {error.name or str(error)!r}. Call functions by name, such as sqrt(2), "
            "ln(5) or sin(pi/6), and use the constants pi and e."
        ) from None
    except ZeroDivisionError:
        raise CalculatorError("division by zero") from None
    except OverflowError:
        raise CalculatorError("the result is too large") from None
    except ValueError as error:
        if "domain" in str(error):
            raise CalculatorError(
                "math domain error: an input is outside the function's domain, "
                "such as the square root or log of a negative number"
            ) from None
        raise CalculatorError(str(error)) from None
    except Exception as error:  # noqa: BLE001 — every failure goes back to the model as text
        raise CalculatorError(str(error) or type(error).__name__) from None


def execute_calculator(expression: str) -> str:
    return calculate(expression)


def execute_math_tool(name: str, arguments: Mapping[str, Any] | None) -> str:
    source = code_from_arguments(arguments)
    if is_calculator_tool_name(name) and is_calculator_expression(source):
        return calculate(source)
    return default_python_session().run(source)


def _calculator_text(expression: str) -> str:
    """Calculator notation as Python: ^ as **, n! as factorial(n), √, π, × and ÷ spelled out."""
    text = expression.strip().translate(_SYMBOLS)
    if text.endswith("=") and not text.endswith("=="):
        text = text[:-1].rstrip()
    if not text:
        raise CalculatorError("the expression is empty")
    if len(text) > MAX_EXPRESSION_CHARS:
        raise CalculatorError(f"the expression is longer than {MAX_EXPRESSION_CHARS:,} characters")
    text = re.sub(r"√\s*(?=\()", "sqrt", text)
    text = re.sub(r"√\s*(\d+(?:\.\d*)?|[A-Za-z_]\w*)", r"sqrt(\1)", text)
    text = re.sub(r"\bmod\b", "%", text)
    return _expand_factorials(text.replace("^", "**"))


def _expand_factorials(text: str) -> str:
    """Rewrite postfix factorials, 5! or (n + 1)!, as factorial() calls. != stays a comparison."""
    position = 0
    while (index := text.find("!", position)) >= 0:
        if text.startswith("!=", index):
            position = index + 2
            continue
        start = _operand_start(text, index)
        if start is None:
            raise CalculatorError("! needs a number, a name or a parenthesized expression before it, as in 5! or (n + 1)!")
        text = f"{text[:start]}factorial({text[start:index].strip()}){text[index + 1:]}"
        position = start
    return text


def _operand_start(text: str, end: int) -> int | None:
    """Where the operand that ends just before `end` starts: a number, a name, a call or a parenthesized group."""
    index = end
    while index > 0 and text[index - 1].isspace():
        index -= 1
    stop = index
    if index > 0 and text[index - 1] == ")":
        depth = 0
        cursor = index - 1
        while cursor >= 0:
            if text[cursor] == ")":
                depth += 1
            elif text[cursor] == "(":
                depth -= 1
                if depth == 0:
                    break
            cursor -= 1
        if cursor < 0:
            return None
        index = cursor
    while index > 0 and (text[index - 1].isalnum() or text[index - 1] in "_."):
        index -= 1
    return index if index < stop else None


class _GuardedPower(ast.NodeTransformer):
    """Send every power through `_power`, which keeps huge exact integers from stalling the worker."""

    def visit_BinOp(self, node: ast.BinOp) -> ast.AST:
        self.generic_visit(node)
        if isinstance(node.op, ast.Pow):
            return ast.Call(func=ast.Name(id=_POWER, ctx=ast.Load()), args=[node.left, node.right], keywords=[])
        return node


def _power(base: Any, exponent: Any, modulus: Any = None) -> Any:
    if modulus is not None:
        return pow(base, exponent, modulus)
    if (
        type(base) is int
        and type(exponent) is int
        and exponent > 0
        and abs(base) > 1
        and exponent * math.log2(abs(base)) > _MAX_EXACT_POWER_BITS
    ):
        return float(base) ** exponent
    return base**exponent


def _factorial(value: Any) -> Any:
    if isinstance(value, float) and value.is_integer():
        value = int(value)
    if isinstance(value, int) and not isinstance(value, bool):
        if value < 0:
            raise CalculatorError("factorial is not defined for negative integers")
        if value > _MAX_FACTORIAL:
            raise CalculatorError(f"factorial({value}) is too large; lgamma({value} + 1) gives its natural log")
        return math.factorial(value)
    return math.gamma(value + 1)


def _values(arguments: tuple[Any, ...]) -> list[Any]:
    """An aggregate's numbers, given separately or as one list: mean(1, 2, 3) or mean([1, 2, 3])."""
    if len(arguments) == 1 and isinstance(arguments[0], Iterable) and not isinstance(arguments[0], (str, bytes)):
        return list(arguments[0])
    return list(arguments)


def _root(value: Any, degree: Any) -> Any:
    """The real nth root, so root(-8, 3) is -2."""
    if isinstance(value, (int, float)) and value < 0 and float(degree).is_integer() and int(degree) % 2 == 1:
        return -((-value) ** (1 / degree))
    return value ** (1 / degree)


def _calculator_functions() -> dict[str, Any]:
    names: dict[str, Any] = {key: getattr(math, key) for key in dir(math) if not key.startswith("_")}
    names.update(
        {
            "ln": lambda value: math.log(value),
            "arcsin": math.asin,
            "arccos": math.acos,
            "arctan": math.atan,
            "arctan2": math.atan2,
            "sec": lambda value: 1 / math.cos(value),
            "csc": lambda value: 1 / math.sin(value),
            "cot": lambda value: 1 / math.tan(value),
            "sign": lambda value: (value > 0) - (value < 0),
            "sgn": lambda value: (value > 0) - (value < 0),
            "root": _root,
            "factorial": _factorial,
            "fact": _factorial,
            "nCr": math.comb,
            "choose": math.comb,
            "nPr": math.perm,
            "pow": _power,
            "abs": abs,
            "round": round,
            "int": int,
            "float": float,
            "divmod": divmod,
            "range": range,
            "len": len,
            "sorted": sorted,
            "list": list,
            "tuple": tuple,
            "sum": lambda *values: sum(_values(values)),
            "min": lambda *values: min(_values(values)),
            "max": lambda *values: max(_values(values)),
            "prod": lambda *values: math.prod(_values(values)),
            "product": lambda *values: math.prod(_values(values)),
            "mean": lambda *values: statistics.fmean(_values(values)),
            "avg": lambda *values: statistics.fmean(_values(values)),
            "average": lambda *values: statistics.fmean(_values(values)),
            "median": lambda *values: statistics.median(_values(values)),
            "mode": lambda *values: statistics.mode(_values(values)),
            "stdev": lambda *values: statistics.stdev(_values(values)),
            "std": lambda *values: statistics.stdev(_values(values)),
            "pstdev": lambda *values: statistics.pstdev(_values(values)),
            "variance": lambda *values: statistics.variance(_values(values)),
            "var": lambda *values: statistics.variance(_values(values)),
            "pvariance": lambda *values: statistics.pvariance(_values(values)),
            "phi": (1 + math.sqrt(5)) / 2,
            "deg": math.pi / 180,
            "math": math,
        }
    )
    return names


def _calculator_result(value: Any) -> str:
    if value is None or isinstance(value, bool):
        return str(value)
    if isinstance(value, int):
        return _exact_integer(value)
    if isinstance(value, float):
        if math.isnan(value):
            return "nan"
        if math.isinf(value):
            return "inf" if value > 0 else "-inf"
        if value.is_integer() and abs(value) < 1e15:
            return str(int(value))
        return format(value, ".12g")
    if isinstance(value, complex):
        if value.imag == 0:
            return _calculator_result(value.real)
        if value.real == 0:
            return f"{_calculator_result(value.imag)}i"
        sign = "-" if value.imag < 0 else "+"
        return f"{_calculator_result(value.real)} {sign} {_calculator_result(abs(value.imag))}i"
    if isinstance(value, (list, tuple, set, frozenset, range, Iterator)):
        items = list(itertools.islice(iter(value), _MAX_LISTED_ITEMS + 1))
        shown = ", ".join(_calculator_result(item) for item in items[:_MAX_LISTED_ITEMS])
        if len(items) > _MAX_LISTED_ITEMS:
            hidden = f"{len(value) - _MAX_LISTED_ITEMS:,} more" if isinstance(value, (list, tuple, set, frozenset, range)) else "more"
            shown = f"{shown}, … ({hidden})"
        return f"({shown})" if isinstance(value, tuple) else f"[{shown}]"
    return str(value)


def _exact_integer(value: int) -> str:
    if value.bit_length() <= _MAX_EXACT_INTEGER_BITS:
        return str(value)
    magnitude = abs(value)
    shift = max(int(math.log10(magnitude)) - 12, 0)
    head = str(magnitude // 10**shift)
    digits = len(head) + shift
    mantissa = f"{head[0]}.{head[1:13]}".rstrip("0").rstrip(".")
    sign = "-" if value < 0 else ""
    return f"{sign}{mantissa}e+{digits - 1} (an integer with {digits:,} digits)"


def _reject_unsafe_ast(tree: ast.AST) -> None:
    for node in ast.walk(tree):
        if isinstance(node, (ast.ClassDef, ast.AsyncFunctionDef)):
            raise CodeExecError("class definitions are blocked")
        if isinstance(node, ast.Attribute) and node.attr.startswith("_"):
            raise CodeExecError("dunder attribute access is blocked")
        if isinstance(node, ast.Name) and node.id.startswith("_"):
            raise CodeExecError("dunder names are blocked")
        if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
            if node.func.id in _BANNED_CALLS:
                raise CodeExecError(f"{node.func.id}() is blocked")


_BANNED_CALLS = frozenset(
    {
        "eval",
        "exec",
        "compile",
        "open",
        "input",
        "breakpoint",
        "getattr",
        "setattr",
        "delattr",
        "globals",
        "locals",
        "vars",
        "dir",
        "help",
        "__import__",
        "memoryview",
        "exit",
        "quit",
    }
)


def _format_result(value: Any) -> str:
    if value is None:
        return "None"
    if isinstance(value, bool):
        return str(value)
    if isinstance(value, float):
        if value.is_integer() and abs(value) < 1e15:
            return str(int(value))
        return format(value, ".12g")
    return str(value)


_ALLOWED_IMPORT_ROOTS = frozenset(
    {
        "cmath",
        "collections",
        "copy",
        "decimal",
        "fractions",
        "functools",
        "itertools",
        "json",
        "math",
        "mpmath",
        "numbers",
        "numpy",
        "operator",
        "re",
        "statistics",
        "string",
        "sympy",
        "textwrap",
        "unicodedata",
    }
)

_WORKER_NAMESPACE: dict[str, Any] | None = None


def _allowed_import(
    name: str,
    globals: dict[str, Any] | None = None,
    locals: dict[str, Any] | None = None,
    fromlist: tuple[str, ...] = (),
    level: int = 0,
) -> Any:
    root = name.split(".")[0]
    if root not in _ALLOWED_IMPORT_ROOTS:
        raise ImportError(f"import of {name!r} is blocked")
    return __import__(name, globals, locals, fromlist, level)


def _worker_namespace() -> dict[str, Any]:
    global _WORKER_NAMESPACE
    if _WORKER_NAMESPACE is None:
        builtins = {
            "abs": abs,
            "all": all,
            "any": any,
            "bin": bin,
            "bool": bool,
            "bytes": bytes,
            "chr": chr,
            "complex": complex,
            "dict": dict,
            "divmod": divmod,
            "enumerate": enumerate,
            "filter": filter,
            "float": float,
            "format": format,
            "frozenset": frozenset,
            "hex": hex,
            "int": int,
            "isinstance": isinstance,
            "issubclass": issubclass,
            "iter": iter,
            "len": len,
            "list": list,
            "map": map,
            "max": max,
            "min": min,
            "next": next,
            "oct": oct,
            "ord": ord,
            "pow": pow,
            "print": print,
            "range": range,
            "repr": repr,
            "reversed": reversed,
            "round": round,
            "set": set,
            "slice": slice,
            "sorted": sorted,
            "str": str,
            "sum": sum,
            "tuple": tuple,
            "zip": zip,
            "True": True,
            "False": False,
            "None": None,
            "__import__": _allowed_import,
        }
        _WORKER_NAMESPACE = {
            "__builtins__": builtins,
            "__name__": "__tool__",
            "math": math,
        }
    return _WORKER_NAMESPACE


def run_cell(code: str) -> str:
    tree = ast.parse(code)
    _reject_unsafe_import_roots(tree)
    _reject_unsafe_ast(tree)
    namespace = _worker_namespace()
    buffer = io.StringIO()
    previous = sys.stdout
    sys.stdout = buffer
    try:
        if tree.body and isinstance(tree.body[-1], ast.Expr):
            body = tree.body[:-1]
            last = tree.body[-1]
            if body:
                exec(  # noqa: S102
                    compile(ast.Module(body, type_ignores=[]), "<tool>", "exec"),
                    namespace,
                    namespace,
                )
            value = eval(  # noqa: S307
                compile(ast.Expression(last.value), "<tool>", "eval"),
                namespace,
                namespace,
            )
            printed = buffer.getvalue()
            if value is None:
                return printed if printed else "None"
            rendered = _format_result(value)
            return f"{printed}{rendered}" if printed else rendered
        exec(compile(tree, "<tool>", "exec"), namespace, namespace)  # noqa: S102
        printed = buffer.getvalue()
        return printed if printed else "None"
    finally:
        sys.stdout = previous


def notebook_variables() -> dict[str, Any]:
    """Names this worker's notebook defined, which calculator calls in the same conversation can use."""
    return {key: value for key, value in _worker_namespace().items() if not key.startswith("_") and key != "math"}


def _reject_unsafe_import_roots(tree: ast.AST) -> None:
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for alias in node.names:
                root = alias.name.split(".")[0]
                if root not in _ALLOWED_IMPORT_ROOTS:
                    raise CodeExecError(f"import of {alias.name!r} is blocked")
        elif isinstance(node, ast.ImportFrom):
            root = (node.module or "").split(".")[0]
            if root not in _ALLOWED_IMPORT_ROOTS:
                raise CodeExecError(f"import of {node.module!r} is blocked")


def _worker_loop() -> None:
    for line in sys.stdin:
        line = line.strip()
        if not line:
            continue
        try:
            request = json.loads(line)
            if "expression" in request:
                output = calculate(str(request.get("expression") or ""), notebook_variables())
            else:
                output = run_cell(str(request.get("code") or ""))
            sys.stdout.write(json.dumps({"ok": True, "output": output}, ensure_ascii=False) + "\n")
        except Exception as error:
            detail = str(error) or type(error).__name__
            sys.stdout.write(
                json.dumps({"ok": False, "output": detail}, ensure_ascii=False) + "\n"
            )
        sys.stdout.flush()


if __name__ == "__main__":
    _worker_loop()