Skip to content

afnio

afnio.MultiTurnMessages = List[Dict[str, List[Variable]]] module-attribute

Type alias for a list of dictionaries representing multi-turn chat messages.

Each message is a mapping with:

  • role: Sender role (e.g. "user", "assistant", "system").
  • content: List of Variable parts representing the message content.

Examples:

>>> msgs: MultiTurnMessages = [
...     {"role": "system", "content": [afnio.Variable("You are a helpful assistant.")]},
...     {"role": "user", "content": [afnio.Variable("Hello")]}
... ]

afnio.Variable

A class to represent generic data, such as textual inputs, outputs, or numeric data.

Attributes:

Name Type Description
data str | int | float | list[str | int | float] | None

The raw data, which can be a scalar string or number, or a sequence of such scalars.

role str

A specific description of the role of the variable in the agent that provides context to the optimizer.

requires_grad bool

Whether to track operations for automatic differentiation and compute gradients.

grad list[Variable]

Stores the gradient of the variable, if requires_grad is set to True and backpropagation has been performed.

Source code in afnio/_variable.py
 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
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
class Variable:
    """
    A class to represent generic data, such as textual inputs, outputs, or numeric data.

    Attributes:
        data: The raw data, which can be a scalar string or number, or a sequence of
            such scalars.
        role: A specific description of the role of the variable in the agent that
            provides context to the optimizer.
        requires_grad: Whether to track operations for automatic differentiation and
            compute gradients.
        grad: Stores the gradient of the variable, if `requires_grad` is set to `True`
            and backpropagation has been performed.
    """

    # NOTE: Using forward references for `Variable` and `Node` in type hints below to
    #       avoid runtime circular imports

    _data: Optional[Union[str, int, float, List[Union[str, int, float]]]]
    role: str
    """
    A specific description of the role of the variable in the agent. For example, it
    could be "system prompt for sentiment classification", "input email or message",
    "output categories", etc.

    It is used to give more context about the purpose of the Variable to the
    [Optimizer][...optim.optimizer.Optimizer].
    """
    requires_grad: bool
    """
    Is ``True`` if gradients need to be computed for this Variable, ``False`` otherwise.

    Note:
        The fact that gradients need to be computed for a Variable does not mean that
        the [`grad`][..grad] attribute will be populated, see [`is_leaf`][..is_leaf] for
        more details.
    """
    _grad: List["Variable"]

    # TODO: Consider having `VariableMeta` class with `grad_fn` and `output_nr`
    #       as attributes
    _output_nr: Optional[int]
    _grad_fn: Optional["Node"]

    _retain_grad: bool
    is_leaf: bool
    """All Variables that have [`requires_grad`][..requires_grad] which is `False`
    will be leaf Variables by convention.

    For Variables that have [`requires_grad`][..requires_grad] which is ``True``, they
    will be leaf Variables if they were created by the user. This means that they are
    not the result of an operation and so `grad_fn` is None.

    Only leaf Variables will have their [`grad`][..grad] populated during a call to
    [`backward`][..backward]. To get [`grad`][..grad] populated for non-leaf Variables,
    you can use [`retain_grad`][..retain_grad].

    Examples:
        >>> a = afnio.Variable("abc", requires_grad=True)
        >>> a.is_leaf
        True
        >>> b = afnio.Variable("abc", requires_grad=True).upper()
        >>> b.is_leaf
        False
        # b was created by the operation that converts all string characters to uppercase
        >>> c = afnio.Variable("abc", requires_grad=True) + "def"
        >>> c.is_leaf
        False
        # c was created by the addition operation
        >>> d = afnio.Variable("abc").upper()
        >>> d.is_leaf
        True
        # d does not require gradients and so has no operation creating it (that is tracked by the autodiff engine)
        >>> e = afnio.Variable("abc").upper().requires_grad_()
        >>> e.is_leaf
        True
        # e requires gradients and has no operations creating it
    """  # noqa: E501
    variable_id: Optional[str]
    _initialized: bool
    _pending_grad_fn_id: Optional[str]
    _pending_grad: Optional[bool]
    _pending_data: Optional[bool]

    def __init__(
        self,
        data: Optional[Union[str, int, float, List[Union[str, int, float]]]] = "",
        role: str = "",
        requires_grad: bool = False,
    ):
        """
        Initializes a `Variable` instance.

        Args:
            data: The raw data for the variable, which can be a scalar string or number,
                or a sequence of such scalars.
            role: A specific description of the role of the variable in the agent that
                provides context to the [`Optimizer`][...optim.optimizer.Optimizer].
            requires_grad: Whether to track operations for automatic differentiation
                and compute gradients.
        """
        # Websocket attributes
        self.variable_id = None
        self._initialized = False  # Falgs variable is ready to send websocket updates
        self._pending_grad_fn_id = None  # Flags grad_fn is being set (fwd pass running)
        self._pending_grad = False  # Flags grad is being set (bwd pass running)
        self._pending_data = False  # Flags data is being set (optim step running)
        # Internal attributes
        self._data = _validate_and_normalize_data(data)
        self.role = role
        self.requires_grad = requires_grad
        self._retain_grad = False
        self._grad = []
        self._output_nr = 0
        self._grad_fn = None
        self.is_leaf = not requires_grad or self.grad_fn is None

        # Share the variable with the websocket server
        if not is_variable_notify_suppressed():
            try:
                from afnio.cognitive.parameter import Parameter

                # Get the singleton websocket client
                _, ws_client = get_default_clients()

                payload = {
                    "data": self.data,
                    "role": self.role,
                    "requires_grad": self.requires_grad,
                    "obj_type": (
                        "__parameter__"
                        if isinstance(self, Parameter)
                        else "__variable__"
                    ),
                }
                response = run_in_background_loop(
                    ws_client.call("create_variable", payload)
                )
                if "error" in response:
                    raise RuntimeError(
                        response["error"]["data"].get("exception", response["error"])
                    )

                logger.debug(f"Variable created and shared with the server: {self!r}")
                variable_id = response.get("result", {}).get("variable_id")
                if not variable_id:
                    raise RuntimeError(
                        f"Server did not return a variable_id "
                        f"for payload: {payload!r}, response: {response!r}"
                    )
                self.variable_id = variable_id
                self._initialized = True
                register_variable(self)
            except Exception as e:
                logger.error(f"Failed to share Variable with the server: {e}")
                raise

    # TODO: pretty print data lists
    def __repr__(self):
        if self._grad_fn:
            return f"Variable(data={self.data}, role={self.role}, grad_fn={self._grad_fn.name()})"  # noqa: E501
        return f"Variable(data={self.data}, role={self.role}, requires_grad={self.requires_grad})"  # noqa: E501

    # TODO: pretty print data lists
    def __str__(self):

        # Helper function to truncate a string if it's longer than 40 characters
        def truncate_str(s):
            if isinstance(s, (int, float)):
                return str(s)
            if len(s) > 40:
                return f"{s[:20]}...{s[-20:]}"
            return s

        # Helper function to show the first and last three elements if it is long
        def format_list(data_list):
            if len(data_list) > 6:
                truncated = [
                    truncate_str(d) for d in (data_list[:3] + ["..."] + data_list[-3:])
                ]
                return f"[{', '.join(truncated)}]"
            return f"[{', '.join(truncate_str(d) for d in data_list)}]"

        if isinstance(self.data, list):
            data_repr = format_list(self.data)
        else:
            data_repr = truncate_str(self.data)

        if self._grad_fn:
            return f"variable({data_repr}, role={truncate_str(self.role)}, grad_fn={self._grad_fn.name()})"  # noqa: E501
        return f"variable({data_repr}, role={truncate_str(self.role)}, requires_grad={self.requires_grad})"  # noqa: E501

    def __add__(self, other) -> "Variable":
        if not isinstance(other, Variable):
            raise TypeError("Only Variables can be added to each other.")

        from afnio.autodiff.basic_ops import Add

        return Add.apply(self, other)

    def __iadd__(self, other) -> "Variable":
        if not isinstance(other, Variable):
            raise TypeError("Only Variables can be added to each other.")

        from afnio.autodiff.basic_ops import Add

        result = Add.apply(self, other)

        self.data = result.data
        self.role = result.role
        self.requires_grad = result.requires_grad

        # Update the grad function in case `other` also has `requires_grad`
        if result.requires_grad:
            with _allow_grad_fn_assignment():
                self.grad_fn = result.grad_fn

        return self

    def backward(
        self,
        gradient: Optional["Variable"] = None,
        retain_graph: Optional[bool] = None,
        create_graph: Optional[bool] = False,
        inputs: Optional[Union["Variable", Sequence["Variable"]]] = None,
    ) -> None:
        """Computes the gradient of current variable with respect to graph leaves.

        The graph is differentiated using the chain rule. If the variable is non-scalar
        (i.e. its data has more than one element) and requires gradient, the function
        additionally requires specifying a `gradient`. It should be a variable with
        data of matching type and shape, that represents the gradient of the
        differentiated function with respect to `self`.

        This function accumulates gradients in the leaf variables; each call to
        `backward` appends new gradient values to the [`grad`][..grad] list. Clear
        existing gradients before calling it again if accumulation is not desired.

        Note:
            When `inputs` are provided, each input must be a leaf variable. If any
            input is not a leaf, a `RuntimeError` is raised.

        Args:
            gradient: The gradient of the function being differentiated with respect to
                `self`. This argument can be omitted if `self` is a scalar.
            retain_graph: If `False`, the graph used to compute the grads will be freed.
                Setting this to `True` retains the graph, allowing for additional
                backward calls on the same graph, useful for example for multi-task
                learning where you have multiple losses. However, retaining the graph is
                not needed in nearly all cases and can be worked around in a much more
                efficient way. Defaults to the value of `create_graph`.
            create_graph: If `True`, graph of the derivative will be constructed,
                allowing to compute higher order derivative products.
            inputs: Inputs with respect to which the gradient will be accumulated into
                [`grad`][..grad]. All other variables will be ignored.
                If not provided, the gradient is accumulated into all the leaf Variables
                that were used to compute the `Variable`s.

        Raises:
            RuntimeError: If the variable is a leaf (i.e., does not require grad or
                does not have a `grad_fn`).
        """

        if self.is_leaf:
            raise RuntimeError(
                "Variable does not require grad or does not have a grad_fn."
            )

        afnio.autodiff.backward(
            self, gradient, retain_graph, create_graph, inputs=inputs
        )

    def requires_grad_(self, mode: bool = True) -> "Variable":
        """
        Change if autodiff should record operations on this variable: sets this
        variable's [`requires_grad`][..requires_grad] attribute in-place.
        Returns this variable.

        [`requires_grad_`][.]'s main use case is to tell autodiff to begin recording
        operations on a Variable `variable`. If `variable` has `requires_grad=False`
        (because it was obtained through a DataLoader, or required preprocessing or
        initialization), `variable.requires_grad_()` makes it so that autodiff will
        begin to record operations on `variable`.

        Args:
            mode: If autodiff should record operations on this variable.

        Returns:
            self: The variable with updated `requires_grad` and `is_leaf` attributes.

        Examples:
            >>> # Initialize with requires_grad=False for data preprocessing
            >>> x = afnio.Variable(data="abc", role="input")
            >>> x = preprocess(x)  # Preprocess without gradient tracking
            >>> x
            variable(abc, role=input, requires_grad=False)
            ...
            >>> # Now enable requires_grad for backpropagation
            >>> x.requires_grad_()
            >>> output = model(x)
            >>> output.backward()  # Backpropagation through `x`
            >>> x.grad
            variable(ABC, role=input, requires_grad=True)
        """
        self.requires_grad = mode
        self.is_leaf = not self.requires_grad or self.grad_fn is None
        return self

    @property
    def data(self) -> Optional[Union[str, int, float, List[Union[str, int, float]]]]:
        """
        The raw data of the Variable.

        Can be either:

        - a scalar: `str`, `int` or `float`, or
        - a sequence: a `list` or `tuple` of scalars.

        Sequence values must be homogeneous: either all strings or all numbers
        (ints/floats). Tuples are converted to lists; if a numeric sequence mixes
        ints and floats the values are promoted to float. Heterogeneous sequences
        mixing strings and numbers are rejected.

        Returns:
            The underlying data value (a scalar or a list of scalars).
        """  # noqa: E501
        self._wait_for_pending(
            "_pending_data"
        )  # Wait until the pending flag is cleared
        return self._data

    @data.setter
    def data(self, value):
        self._data = _validate_and_normalize_data(value)

    @property
    def output_nr(self) -> int:
        return self._output_nr

    @output_nr.setter
    def output_nr(self, n: int):
        if not isinstance(n, int) or not (n >= 0):
            raise TypeError(
                f"`output_nr` can only be an int greater or equal to 0, "
                f"but {n} is of type {type(n).__name__}"
            )
        self._output_nr = n

    @property
    def grad_fn(self) -> Optional["Node"]:
        self._wait_for_pending(
            "_pending_grad_fn_id"
        )  # Wait until the pending flag is cleared
        return self._grad_fn

    @grad_fn.setter
    def grad_fn(self, fn: Callable):
        """
        Sets the `grad_fn` that will be called by the engine to produce the actual
        gradient for this variable.
        """
        if not getattr(_grad_fn_assignment_allowed, "value", False):
            raise AttributeError(
                "Direct assignment to `grad_fn` is not allowed. "
                "Use Function.apply() to construct Variables with a grad_fn."
            )
        if not self.requires_grad:
            raise RuntimeError(
                "Cannot set `grad_fn` on a variable that does not require gradients. "
                "To enable gradient tracking for this variable, call "
                "`requires_grad_()` before setting `grad_fn`. Only variables with "
                "`requires_grad=True` can have a gradient function (`grad_fn`)."
            )
        self._grad_fn = fn
        self.is_leaf = not self.requires_grad or self.grad_fn is None

    @property
    def grad(self) -> List["Variable"]:
        """
        This attribute is an empty `list` by default and becomes a `list` of
        [`Variable`][..]s the first time a call to [`backward`][..backward] computes
        gradients for `self`. The attribute will then contain the gradients computed and
        future calls to [`backward`][..backward] will accumulate (append) gradients
        into it.

        Raises:
            RuntimeError: If the variable is a non-leaf without `retain_grad` enabled.
        """
        self._wait_for_pending(
            "_pending_grad"
        )  # Wait until the pending flag is cleared
        if self.is_leaf or self._retain_grad:
            return self._grad
        else:
            # Throwing a `UserWarning`` instead of `RuntimeError` could do here, like
            # in Pytorch, but for now I cannot think of any use case for not throwing
            # the error
            raise RuntimeError(
                "Attempted to access `grad` for a non-leaf Variable without "
                "`retain_grad` enabled. Non-leaf Variables do not have their gradients "
                "retained by default in autodiff. To retain gradients for this "
                "Variable, call `retain_grad()` before performing the backward pass."
            )

    @grad.setter
    def grad(self, gradient: List["Variable"]):
        """
        Sets the `grad` for this variable if it is a leaf or has `retain_grad` enabled.
        """
        if not isinstance(gradient, list) or not all(
            isinstance(g, Variable) for g in gradient
        ):
            raise TypeError(
                f"`grad` expects a list of Variables for the gradient to accumulate, "
                f"but got {type(gradient).__name__}."
            )

        if self.is_leaf or self._retain_grad:
            self._grad = gradient
        else:
            # Throwing a `UserWarning`` instead of `RuntimeError` could do here, like
            # in Pytorch, but for now I cannot think of any use case for not throwing
            # the error
            raise RuntimeError(
                "Attempted to set `grad` for a non-leaf Variable without `retain_grad` "
                "enabled. Non-leaf Variables do not have their gradients retained by "
                "default in autodiff. To retain gradients for this Variable, call "
                "`retain_grad()` before performing the backward pass."
            )

    def append_grad(self, gradient: "Variable"):
        """
        Appends a gradient value to the list [`grad`][..grad] for this variable.

        Args:
            gradient: The gradient variable to append.

        Raises:
            RuntimeError: If the variable is a non-leaf without `retain_grad` enabled.
        """
        if self.is_leaf or self._retain_grad:
            self._on_append_grad(gradient)
            self._grad.append(gradient)
        else:
            # Throwing a `UserWarning`` instead of `RuntimeError` could do here, like
            # in Pytorch, but for now I cannot think of any use case for not throwing
            # the error
            raise RuntimeError(
                "Attempted to append to `grad` for a non-leaf Variable without "
                "`retain_grad` enabled. Non-leaf Variables do not have their gradients "
                "retained by default in autodiff. To retain gradients for this "
                "Variable, call `retain_grad()` before performing the backward pass."
            )

    def retain_grad(self):
        """
        Enables this Variable to have their [`grad`][..grad] populated during
        [`backward()`][..backward]. This is a no-op for leaf Variables.

        Raises:
            RuntimeError: If the variable is a leaf.
        """
        if not self.is_leaf:
            self._retain_grad = True
        else:
            raise RuntimeError("Cannot call `retain_grad` on a leaf variable")

    def detach(self) -> "Variable":
        """
        Returns a new Variable, detached from the computation graph.
        This new Variable will not have a `grad_fn` and will not track gradients.

        Returns:
            A new Variable with the same data and role, but with `requires_grad` set to `False`.
        """  # noqa: E501
        return Variable(self.data, role=self.role, requires_grad=False)

    # def clone(self):
    #     """
    #     Create a copy of this Variable, preserving the data.
    #     """
    #     return copy.deepcopy(self)

    def __deepcopy__(self, memo):
        if not self.is_leaf:
            raise RuntimeError(
                "Only Variables created explicitly by the user "
                "(graph leaves) support the deepcopy protocol at the moment."
            )
        if id(self) in memo:
            return memo[id(self)]

        with afnio.no_grad():
            new_variable = Variable(
                data=deepcopy(self.data, memo),
                role=self.role,
                requires_grad=self.requires_grad,
            )

            new_variable._retain_grad = self._retain_grad
            new_variable._output_nr = self._output_nr

            if self.grad_fn:
                with _allow_grad_fn_assignment():
                    new_variable.grad_fn = deepcopy(
                        self.grad_fn, memo
                    )  # Also sets `is_leaf`
            if self.grad != []:
                new_variable.grad = deepcopy(self.grad, memo)

            new_variable.__dict__ = deepcopy(self.__dict__, memo)

            memo[id(self)] = new_variable

            return new_variable

    def copy_(self, src: "Variable") -> "Variable":
        """
        Copies the data from the source Variable into this Variable.

        Args:
            src: The source Variable to copy from.

        Returns:
            self: The current Variable with updated `data`, `role` and `requires_grad`.

        Raises:
            TypeError: If the source is not a Variable.
            ValueError: If the source data type does not match the target data type.
        """
        if not is_variable(src):
            raise TypeError(
                f"Expected `src` to be a Variable, but got {type(src).__name__}."
            )

        is_scalar_self = is_scalar_variable(self)
        is_scalar_src = is_scalar_variable(src)

        if is_scalar_self and is_scalar_src:
            self.data = src.data
        elif not is_scalar_self and not is_scalar_src:
            if len(self.data) != len(src.data):
                raise ValueError(
                    f"Cannot copy list `data` fields of different lengths: "
                    f"{len(self.data)} vs {len(src.data)}."
                )
            self.data = src.data.copy()
        else:
            raise ValueError(
                f"Cannot copy data from {type(src.data).__name__} "
                f"to {type(self.data).__name__}."
            )

        self.role = src.role
        self.requires_grad = src.requires_grad
        return self

    def is_floating_point(self) -> bool:
        """
        Checks if the Variable's [`data`][..data] contains floating-point values.

        Returns:
            `True` if the data is a floating-point type (either scalar or all elements in a list/tuple are floating-point).
        """  # noqa: E501
        if isinstance(self.data, float):
            return True

        if isinstance(self.data, (list, tuple)) and self.data:
            return all(isinstance(d, float) for d in self.data)

        return False

    def to(self, dtype: Optional[type] = None) -> "Variable":
        """
        Cast the [`data`][..data] of the Variable to the specified dtype.

        Args:
            dtype: The target type to cast the data (e.g., `float`, `int`, `str`).

        Returns:
            A new Variable with [`data`][..data] cast to the target `dtype`.
        """
        if dtype is not None:
            if not is_scalar_variable(self):
                # Cast each element in the list to the target dtype
                new_data = [dtype(d) for d in self.data]
            else:
                # Cast scalar data to the target dtype
                new_data = dtype(self.data)
        else:
            # No dtype casting
            new_data = self.data

        # Return a new Variable with the same role and requires_grad, but updated data
        return Variable(data=new_data, role=self.role, requires_grad=self.requires_grad)

    def _on_variable_change(self, field: str, value: Any):
        """
        Notify the server of a change in the variable's attributes.
        This method is called whenever an attribute of the variable is set.
        It sends a notification to the server with the updated field and value.

        Args:
            field: The name of the field that changed.
            value: The new value of the field.

        Raises:
            RuntimeError: If the variable is not registered with the server or if the
                server response does not match the request.
            TypeError: If the provided value is of an unexpected type for the field.
        """
        from afnio._utils import _serialize_arg

        if is_variable_notify_suppressed():
            return  # Do not notify server

        if self.variable_id is None:
            logger.error(
                f"Cannot notify server: "
                f"variable_id=None, field='{field}', value={value!r}"
            )
            raise RuntimeError("Cannot notify server: variable_id is None.")

        if field in {
            "output_nr",
            "grad_fn",
            "grad",
            "_initialized",
            "_pending_grad_fn_id",
            "_pending_grad",
            "_pending_data",
            "__dict__",  # Avoids server error when calling `Optimizer.load_state_dict`
        }:
            # Do not notify for the property setter, as we already notify
            # for all the changes made inside the property setter.
            # Also do not notify for `_initialized` and pending states
            return
        elif field == "_data":
            field = "data"  # `data` is a property only on the client
            end_value = value
        elif field == "_grad":
            if not isinstance(value, list):
                raise TypeError(
                    f"Expected `value` to be a list for field '{field}', "
                    f"but got {type(value).__name__}."
                )
            end_value = [_serialize_arg(g) for g in value]
        elif field == "_grad_fn":
            # Only allow notification if inside the `__iadd__` method
            if not _called_directly_from_iadd():
                raise RuntimeError(
                    "Setting `grad_fn` is only allowed on the server by the autodiff "
                    "engine. Do not use `_allow_grad_fn_assignment()` on the client."
                )
            end_value = value.node_id  # Use only the node ID for notification
        else:
            end_value = value

        payload = {
            "variable_id": self.variable_id,
            "field": field,
            "value": end_value,
        }

        try:
            _, ws_client = get_default_clients()
            response = run_in_background_loop(
                ws_client.call("update_variable", payload)
            )
            if "error" in response:
                raise RuntimeError(
                    response["error"]["data"].get("exception", response["error"])
                )

            # Check server response
            if (
                response["result"]["variable_id"] != self.variable_id
                or response["result"]["field"] != field
                or response["result"]["value"] != end_value
            ):
                raise RuntimeError(
                    f"Server response mismatch: (received {response['result']!r}, "
                    f"but expected variable_id={self.variable_id!r}, field={field!r}, "
                    f"value={end_value!r})"
                )
            logger.debug(
                f"Variable change notified to server and confirmed: "
                f"variable_id={self.variable_id!r}, field='{field}', "
                f"value={end_value!r}"
            )

        except Exception as e:
            logger.exception(f"Failed to notify server of variable change: {e}")
            raise

    def _on_append_grad(self, gradient: "Variable"):
        """
        Notify the server that a new gradient has been appended to this variable.

        This method is called before the gradient is added to the local [`grad`][..grad]
        list. It sends an 'append_grad' RPC request to the server, including the
        variable's ID and the serialized gradient. The method blocks until the server
        acknowledges the append operation, ensuring synchronization between client and
        server.

        Args:
            gradient: The gradient variable to append.

        Raises:
            RuntimeError: If the variable is not registered with the server or if the
                server response does not match the request.
            TypeError: If the provided gradient is not a Variable.
        """
        from afnio._utils import _serialize_arg

        if is_variable_notify_suppressed():
            return  # Do not notify server

        if self.variable_id is None:
            logger.error(
                f"Cannot notify server: variable_id=None, gradient={gradient!r}"
            )
            raise RuntimeError("Cannot notify server: variable_id is None.")

        if not isinstance(gradient, Variable):
            raise TypeError(
                f"Expected `value` to be a Variable, but got {type(gradient).__name__}."
            )

        ser_grad = _serialize_arg(gradient)

        payload = {
            "variable_id": self.variable_id,
            "gradient": ser_grad,
        }

        try:
            _, ws_client = get_default_clients()
            response = run_in_background_loop(ws_client.call("append_grad", payload))
            if "error" in response:
                raise RuntimeError(
                    response["error"]["data"].get("exception", response["error"])
                )

            # Check server response
            if (
                response["result"]["variable_id"] != self.variable_id
                or response["result"]["gradient_id"] != gradient.variable_id
            ):
                raise RuntimeError(
                    f"Server response mismatch: (received {response['result']!r}, "
                    f"but expected variable_id={self.variable_id!r}, "
                    f"gradient={ser_grad!r}"
                )
            logger.debug(
                f"Gradient append notified to server and confirmed: "
                f"variable_id={self.variable_id!r}, gradient={ser_grad!r}"
            )

        except Exception as e:
            logger.exception(f"Failed to notify server of gradient append: {e}")
            raise

    def __setattr__(self, name, value):
        super().__setattr__(name, value)
        if getattr(self, "_initialized", False):
            self._on_variable_change(name, value)
        # TODO: Should we handle the else condition and throw an error?

    def _wait_for_pending(
        self, attr_name: str, timeout: float = 3, interval: float = 0.01
    ) -> None:
        """
        Wait until the attribute specified by `attr_name` is no longer truthy.
        Uses `time.monotonic()` for more reliable timeout measurement.

        Args:
            attr_name: Name of the attribute to wait on.
            timeout: Maximum time to wait, in seconds.
            interval: How frequently to check the attribute, in seconds.

        Raises:
            RuntimeError: If the attribute remains truthy after the timeout.
        """
        end_time = time.monotonic() + timeout
        while getattr(self, attr_name):
            if time.monotonic() > end_time:
                raise RuntimeError(
                    f"Timeout waiting for {attr_name} to be cleared "
                    f"for variable_id={self.variable_id}"
                )
            time.sleep(interval)

role = role instance-attribute

A specific description of the role of the variable in the agent. For example, it could be "system prompt for sentiment classification", "input email or message", "output categories", etc.

It is used to give more context about the purpose of the Variable to the Optimizer.

requires_grad = requires_grad instance-attribute

Is True if gradients need to be computed for this Variable, False otherwise.

Note

The fact that gradients need to be computed for a Variable does not mean that the grad attribute will be populated, see is_leaf for more details.

is_leaf = not requires_grad or self.grad_fn is None instance-attribute

All Variables that have requires_grad which is False will be leaf Variables by convention.

For Variables that have requires_grad which is True, they will be leaf Variables if they were created by the user. This means that they are not the result of an operation and so grad_fn is None.

Only leaf Variables will have their grad populated during a call to backward. To get grad populated for non-leaf Variables, you can use retain_grad.

Examples:

>>> a = afnio.Variable("abc", requires_grad=True)
>>> a.is_leaf
True
>>> b = afnio.Variable("abc", requires_grad=True).upper()
>>> b.is_leaf
False
# b was created by the operation that converts all string characters to uppercase
>>> c = afnio.Variable("abc", requires_grad=True) + "def"
>>> c.is_leaf
False
# c was created by the addition operation
>>> d = afnio.Variable("abc").upper()
>>> d.is_leaf
True
# d does not require gradients and so has no operation creating it (that is tracked by the autodiff engine)
>>> e = afnio.Variable("abc").upper().requires_grad_()
>>> e.is_leaf
True
# e requires gradients and has no operations creating it

data property writable

The raw data of the Variable.

Can be either:

  • a scalar: str, int or float, or
  • a sequence: a list or tuple of scalars.

Sequence values must be homogeneous: either all strings or all numbers (ints/floats). Tuples are converted to lists; if a numeric sequence mixes ints and floats the values are promoted to float. Heterogeneous sequences mixing strings and numbers are rejected.

Returns:

Type Description
str | int | float | list[str | int | float] | None

The underlying data value (a scalar or a list of scalars).

grad property writable

This attribute is an empty list by default and becomes a list of Variables the first time a call to backward computes gradients for self. The attribute will then contain the gradients computed and future calls to backward will accumulate (append) gradients into it.

Raises:

Type Description
RuntimeError

If the variable is a non-leaf without retain_grad enabled.

__init__(data='', role='', requires_grad=False)

Initializes a Variable instance.

Parameters:

Name Type Description Default
data str | int | float | list[str | int | float] | None

The raw data for the variable, which can be a scalar string or number, or a sequence of such scalars.

''
role str

A specific description of the role of the variable in the agent that provides context to the Optimizer.

''
requires_grad bool

Whether to track operations for automatic differentiation and compute gradients.

False
Source code in afnio/_variable.py
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
def __init__(
    self,
    data: Optional[Union[str, int, float, List[Union[str, int, float]]]] = "",
    role: str = "",
    requires_grad: bool = False,
):
    """
    Initializes a `Variable` instance.

    Args:
        data: The raw data for the variable, which can be a scalar string or number,
            or a sequence of such scalars.
        role: A specific description of the role of the variable in the agent that
            provides context to the [`Optimizer`][...optim.optimizer.Optimizer].
        requires_grad: Whether to track operations for automatic differentiation
            and compute gradients.
    """
    # Websocket attributes
    self.variable_id = None
    self._initialized = False  # Falgs variable is ready to send websocket updates
    self._pending_grad_fn_id = None  # Flags grad_fn is being set (fwd pass running)
    self._pending_grad = False  # Flags grad is being set (bwd pass running)
    self._pending_data = False  # Flags data is being set (optim step running)
    # Internal attributes
    self._data = _validate_and_normalize_data(data)
    self.role = role
    self.requires_grad = requires_grad
    self._retain_grad = False
    self._grad = []
    self._output_nr = 0
    self._grad_fn = None
    self.is_leaf = not requires_grad or self.grad_fn is None

    # Share the variable with the websocket server
    if not is_variable_notify_suppressed():
        try:
            from afnio.cognitive.parameter import Parameter

            # Get the singleton websocket client
            _, ws_client = get_default_clients()

            payload = {
                "data": self.data,
                "role": self.role,
                "requires_grad": self.requires_grad,
                "obj_type": (
                    "__parameter__"
                    if isinstance(self, Parameter)
                    else "__variable__"
                ),
            }
            response = run_in_background_loop(
                ws_client.call("create_variable", payload)
            )
            if "error" in response:
                raise RuntimeError(
                    response["error"]["data"].get("exception", response["error"])
                )

            logger.debug(f"Variable created and shared with the server: {self!r}")
            variable_id = response.get("result", {}).get("variable_id")
            if not variable_id:
                raise RuntimeError(
                    f"Server did not return a variable_id "
                    f"for payload: {payload!r}, response: {response!r}"
                )
            self.variable_id = variable_id
            self._initialized = True
            register_variable(self)
        except Exception as e:
            logger.error(f"Failed to share Variable with the server: {e}")
            raise

backward(gradient=None, retain_graph=None, create_graph=False, inputs=None)

Computes the gradient of current variable with respect to graph leaves.

The graph is differentiated using the chain rule. If the variable is non-scalar (i.e. its data has more than one element) and requires gradient, the function additionally requires specifying a gradient. It should be a variable with data of matching type and shape, that represents the gradient of the differentiated function with respect to self.

This function accumulates gradients in the leaf variables; each call to backward appends new gradient values to the grad list. Clear existing gradients before calling it again if accumulation is not desired.

Note

When inputs are provided, each input must be a leaf variable. If any input is not a leaf, a RuntimeError is raised.

Parameters:

Name Type Description Default
gradient Variable | None

The gradient of the function being differentiated with respect to self. This argument can be omitted if self is a scalar.

None
retain_graph bool | None

If False, the graph used to compute the grads will be freed. Setting this to True retains the graph, allowing for additional backward calls on the same graph, useful for example for multi-task learning where you have multiple losses. However, retaining the graph is not needed in nearly all cases and can be worked around in a much more efficient way. Defaults to the value of create_graph.

None
create_graph bool | None

If True, graph of the derivative will be constructed, allowing to compute higher order derivative products.

False
inputs Variable | Sequence[Variable] | None

Inputs with respect to which the gradient will be accumulated into grad. All other variables will be ignored. If not provided, the gradient is accumulated into all the leaf Variables that were used to compute the Variables.

None

Raises:

Type Description
RuntimeError

If the variable is a leaf (i.e., does not require grad or does not have a grad_fn).

Source code in afnio/_variable.py
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
def backward(
    self,
    gradient: Optional["Variable"] = None,
    retain_graph: Optional[bool] = None,
    create_graph: Optional[bool] = False,
    inputs: Optional[Union["Variable", Sequence["Variable"]]] = None,
) -> None:
    """Computes the gradient of current variable with respect to graph leaves.

    The graph is differentiated using the chain rule. If the variable is non-scalar
    (i.e. its data has more than one element) and requires gradient, the function
    additionally requires specifying a `gradient`. It should be a variable with
    data of matching type and shape, that represents the gradient of the
    differentiated function with respect to `self`.

    This function accumulates gradients in the leaf variables; each call to
    `backward` appends new gradient values to the [`grad`][..grad] list. Clear
    existing gradients before calling it again if accumulation is not desired.

    Note:
        When `inputs` are provided, each input must be a leaf variable. If any
        input is not a leaf, a `RuntimeError` is raised.

    Args:
        gradient: The gradient of the function being differentiated with respect to
            `self`. This argument can be omitted if `self` is a scalar.
        retain_graph: If `False`, the graph used to compute the grads will be freed.
            Setting this to `True` retains the graph, allowing for additional
            backward calls on the same graph, useful for example for multi-task
            learning where you have multiple losses. However, retaining the graph is
            not needed in nearly all cases and can be worked around in a much more
            efficient way. Defaults to the value of `create_graph`.
        create_graph: If `True`, graph of the derivative will be constructed,
            allowing to compute higher order derivative products.
        inputs: Inputs with respect to which the gradient will be accumulated into
            [`grad`][..grad]. All other variables will be ignored.
            If not provided, the gradient is accumulated into all the leaf Variables
            that were used to compute the `Variable`s.

    Raises:
        RuntimeError: If the variable is a leaf (i.e., does not require grad or
            does not have a `grad_fn`).
    """

    if self.is_leaf:
        raise RuntimeError(
            "Variable does not require grad or does not have a grad_fn."
        )

    afnio.autodiff.backward(
        self, gradient, retain_graph, create_graph, inputs=inputs
    )

requires_grad_(mode=True)

Change if autodiff should record operations on this variable: sets this variable's requires_grad attribute in-place. Returns this variable.

requires_grad_'s main use case is to tell autodiff to begin recording operations on a Variable variable. If variable has requires_grad=False (because it was obtained through a DataLoader, or required preprocessing or initialization), variable.requires_grad_() makes it so that autodiff will begin to record operations on variable.

Parameters:

Name Type Description Default
mode bool

If autodiff should record operations on this variable.

True

Returns:

Name Type Description
self Variable

The variable with updated requires_grad and is_leaf attributes.

Examples:

>>> # Initialize with requires_grad=False for data preprocessing
>>> x = afnio.Variable(data="abc", role="input")
>>> x = preprocess(x)  # Preprocess without gradient tracking
>>> x
variable(abc, role=input, requires_grad=False)
...
>>> # Now enable requires_grad for backpropagation
>>> x.requires_grad_()
>>> output = model(x)
>>> output.backward()  # Backpropagation through `x`
>>> x.grad
variable(ABC, role=input, requires_grad=True)
Source code in afnio/_variable.py
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
def requires_grad_(self, mode: bool = True) -> "Variable":
    """
    Change if autodiff should record operations on this variable: sets this
    variable's [`requires_grad`][..requires_grad] attribute in-place.
    Returns this variable.

    [`requires_grad_`][.]'s main use case is to tell autodiff to begin recording
    operations on a Variable `variable`. If `variable` has `requires_grad=False`
    (because it was obtained through a DataLoader, or required preprocessing or
    initialization), `variable.requires_grad_()` makes it so that autodiff will
    begin to record operations on `variable`.

    Args:
        mode: If autodiff should record operations on this variable.

    Returns:
        self: The variable with updated `requires_grad` and `is_leaf` attributes.

    Examples:
        >>> # Initialize with requires_grad=False for data preprocessing
        >>> x = afnio.Variable(data="abc", role="input")
        >>> x = preprocess(x)  # Preprocess without gradient tracking
        >>> x
        variable(abc, role=input, requires_grad=False)
        ...
        >>> # Now enable requires_grad for backpropagation
        >>> x.requires_grad_()
        >>> output = model(x)
        >>> output.backward()  # Backpropagation through `x`
        >>> x.grad
        variable(ABC, role=input, requires_grad=True)
    """
    self.requires_grad = mode
    self.is_leaf = not self.requires_grad or self.grad_fn is None
    return self

append_grad(gradient)

Appends a gradient value to the list grad for this variable.

Parameters:

Name Type Description Default
gradient Variable

The gradient variable to append.

required

Raises:

Type Description
RuntimeError

If the variable is a non-leaf without retain_grad enabled.

Source code in afnio/_variable.py
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
def append_grad(self, gradient: "Variable"):
    """
    Appends a gradient value to the list [`grad`][..grad] for this variable.

    Args:
        gradient: The gradient variable to append.

    Raises:
        RuntimeError: If the variable is a non-leaf without `retain_grad` enabled.
    """
    if self.is_leaf or self._retain_grad:
        self._on_append_grad(gradient)
        self._grad.append(gradient)
    else:
        # Throwing a `UserWarning`` instead of `RuntimeError` could do here, like
        # in Pytorch, but for now I cannot think of any use case for not throwing
        # the error
        raise RuntimeError(
            "Attempted to append to `grad` for a non-leaf Variable without "
            "`retain_grad` enabled. Non-leaf Variables do not have their gradients "
            "retained by default in autodiff. To retain gradients for this "
            "Variable, call `retain_grad()` before performing the backward pass."
        )

retain_grad()

Enables this Variable to have their grad populated during backward(). This is a no-op for leaf Variables.

Raises:

Type Description
RuntimeError

If the variable is a leaf.

Source code in afnio/_variable.py
488
489
490
491
492
493
494
495
496
497
498
499
def retain_grad(self):
    """
    Enables this Variable to have their [`grad`][..grad] populated during
    [`backward()`][..backward]. This is a no-op for leaf Variables.

    Raises:
        RuntimeError: If the variable is a leaf.
    """
    if not self.is_leaf:
        self._retain_grad = True
    else:
        raise RuntimeError("Cannot call `retain_grad` on a leaf variable")

detach()

Returns a new Variable, detached from the computation graph. This new Variable will not have a grad_fn and will not track gradients.

Returns:

Type Description
Variable

A new Variable with the same data and role, but with requires_grad set to False.

Source code in afnio/_variable.py
501
502
503
504
505
506
507
508
509
def detach(self) -> "Variable":
    """
    Returns a new Variable, detached from the computation graph.
    This new Variable will not have a `grad_fn` and will not track gradients.

    Returns:
        A new Variable with the same data and role, but with `requires_grad` set to `False`.
    """  # noqa: E501
    return Variable(self.data, role=self.role, requires_grad=False)

copy_(src)

Copies the data from the source Variable into this Variable.

Parameters:

Name Type Description Default
src Variable

The source Variable to copy from.

required

Returns:

Name Type Description
self Variable

The current Variable with updated data, role and requires_grad.

Raises:

Type Description
TypeError

If the source is not a Variable.

ValueError

If the source data type does not match the target data type.

Source code in afnio/_variable.py
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
def copy_(self, src: "Variable") -> "Variable":
    """
    Copies the data from the source Variable into this Variable.

    Args:
        src: The source Variable to copy from.

    Returns:
        self: The current Variable with updated `data`, `role` and `requires_grad`.

    Raises:
        TypeError: If the source is not a Variable.
        ValueError: If the source data type does not match the target data type.
    """
    if not is_variable(src):
        raise TypeError(
            f"Expected `src` to be a Variable, but got {type(src).__name__}."
        )

    is_scalar_self = is_scalar_variable(self)
    is_scalar_src = is_scalar_variable(src)

    if is_scalar_self and is_scalar_src:
        self.data = src.data
    elif not is_scalar_self and not is_scalar_src:
        if len(self.data) != len(src.data):
            raise ValueError(
                f"Cannot copy list `data` fields of different lengths: "
                f"{len(self.data)} vs {len(src.data)}."
            )
        self.data = src.data.copy()
    else:
        raise ValueError(
            f"Cannot copy data from {type(src.data).__name__} "
            f"to {type(self.data).__name__}."
        )

    self.role = src.role
    self.requires_grad = src.requires_grad
    return self

is_floating_point()

Checks if the Variable's data contains floating-point values.

Returns:

Type Description
bool

True if the data is a floating-point type (either scalar or all elements in a list/tuple are floating-point).

Source code in afnio/_variable.py
591
592
593
594
595
596
597
598
599
600
601
602
603
604
def is_floating_point(self) -> bool:
    """
    Checks if the Variable's [`data`][..data] contains floating-point values.

    Returns:
        `True` if the data is a floating-point type (either scalar or all elements in a list/tuple are floating-point).
    """  # noqa: E501
    if isinstance(self.data, float):
        return True

    if isinstance(self.data, (list, tuple)) and self.data:
        return all(isinstance(d, float) for d in self.data)

    return False

to(dtype=None)

Cast the data of the Variable to the specified dtype.

Parameters:

Name Type Description Default
dtype type | None

The target type to cast the data (e.g., float, int, str).

None

Returns:

Type Description
Variable

A new Variable with data cast to the target dtype.

Source code in afnio/_variable.py
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
def to(self, dtype: Optional[type] = None) -> "Variable":
    """
    Cast the [`data`][..data] of the Variable to the specified dtype.

    Args:
        dtype: The target type to cast the data (e.g., `float`, `int`, `str`).

    Returns:
        A new Variable with [`data`][..data] cast to the target `dtype`.
    """
    if dtype is not None:
        if not is_scalar_variable(self):
            # Cast each element in the list to the target dtype
            new_data = [dtype(d) for d in self.data]
        else:
            # Cast scalar data to the target dtype
            new_data = dtype(self.data)
    else:
        # No dtype casting
        new_data = self.data

    # Return a new Variable with the same role and requires_grad, but updated data
    return Variable(data=new_data, role=self.role, requires_grad=self.requires_grad)

afnio.GradientEdge

Bases: NamedTuple

Object representing a given gradient edge within the autodiff backward graph.

A GradientEdge identifies where a gradient should be propagated during backpropagation. It points to a specific backward node and a specific output index of that node.

Each Variable that participates in autodiff is associated with one or more GradientEdge objects, which define how gradients flow through the computation graph.

To get the gradient edge where a given Variable gradient will be computed, you can do edge = autodiff.graph.get_gradient_edge(variable).

Attributes:

Name Type Description
node Node

The backward node responsible for producing gradients. This is an instance of a backward Function (e.g. AccumulateGrad, SumBackward0, ChatCompletionBackward0).

output_nr int

The index of the output of node to which this edge corresponds. Required because a backward node may produce multiple gradient outputs.

Source code in afnio/autodiff/graph.py
 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
class GradientEdge(NamedTuple):
    """
    Object representing a given gradient edge within the autodiff backward graph.

    A GradientEdge identifies *where* a gradient should be propagated during
    backpropagation. It points to a specific backward node and a specific
    output index of that node.

    Each [`Variable`][..Variable] that participates in autodiff is associated with
    one or more GradientEdge objects, which define how gradients flow through the
    computation graph.

    To get the gradient edge where a given Variable gradient will be computed,
    you can do `edge = autodiff.graph.get_gradient_edge(variable)`.

    Attributes:
        node: The backward node responsible for producing gradients.
            This is an instance of a backward Function (e.g. AccumulateGrad,
            SumBackward0, ChatCompletionBackward0).
        output_nr: The index of the output of `node` to which this edge corresponds.
            Required because a backward node may produce multiple gradient
            outputs.
    """

    node: Node
    output_nr: int

    def __repr__(self):
        name = (
            f"<{self.node.name()} object at {hex(id(self.node))}>"
            if self.node
            else "None"
        )
        return f"({name}, {self.output_nr})"

    def __str__(self):
        name = (
            f"<{self.node.name()} object at {hex(id(self.node))}>"
            if self.node
            else "None"
        )
        return f"({name}, {self.output_nr})"

afnio.Node

Base class for nodes in the autograd computation graph.

A Node represents a single operation in the backward graph and is responsible for producing gradients during backpropagation. Each Node may have multiple inputs and outputs and is connected to other nodes via GradientEdge objects.

Attributes:

Name Type Description
next_functions tuple[GradientEdge]

The gradient edges to this node's parent nodes in the backward graph.

Source code in afnio/autodiff/graph.py
 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
class Node:
    """
    Base class for nodes in the autograd computation graph.

    A Node represents a single operation in the backward graph and is
    responsible for producing gradients during backpropagation. Each Node
    may have multiple inputs and outputs and is connected to other nodes
    via [`GradientEdge`][..GradientEdge] objects.

    Attributes:
        next_functions: The gradient edges to this node's parent nodes
            in the backward graph.
    """

    def __init__(self, next_functions: Optional[Tuple["GradientEdge"]] = None):
        self._next_functions = next_functions if next_functions else ()
        self._name = None
        self.node_id = None

    def __repr__(self):
        return f"<afnio.autodiff.function.{self.name()} object at {hex(id(self))}>"

    def __str__(self):
        return f"<{self.name()} object at {hex(id(self))}>"

    def apply(self, *args):
        raise NotImplementedError("Subclasses should implement this method.")

    def name(self) -> str:
        """
        Returns the name of the backward node.

        Returns:
            The name of the backward node, which is typically the name of the forward operation that produced the output associated with this node.

        Examples:
            >>> import afnio
            >>> import afnio.cognitive.functional as F
            >>> a = afnio.Variable("Hello,", requires_grad=True)
            >>> b = afnio.Variable("world!", requires_grad=True)
            >>> c = F.sum([a, b])
            >>> assert isinstance(c.grad_fn, afnio.autodiff.graph.Node)
            >>> print(c.grad_fn.name())
            SumBackward0
        """  # noqa: E501
        return self._name

    @property
    def next_functions(self) -> Tuple["GradientEdge"]:
        """
        Returns the gradient edges to this node's parent nodes in the backward graph.

        Each entry is a tuple of `(Node, output_nr)` identifying where gradients
        should be propagated next during backpropagation.

        Returns:
            A tuple of [`GradientEdge`][..GradientEdge] objects representing the edges to this node's parent nodes in the backward graph.
        """  # noqa: E501
        return self._next_functions

    @next_functions.setter
    def next_functions(self, edges: Tuple["GradientEdge", ...]):
        self._next_functions = edges

next_functions property writable

Returns the gradient edges to this node's parent nodes in the backward graph.

Each entry is a tuple of (Node, output_nr) identifying where gradients should be propagated next during backpropagation.

Returns:

Type Description
tuple[GradientEdge]

A tuple of [GradientEdge][afnio.Node.GradientEdge] objects representing the edges to this node's parent nodes in the backward graph.

name()

Returns the name of the backward node.

Returns:

Type Description
str

The name of the backward node, which is typically the name of the forward operation that produced the output associated with this node.

Examples:

>>> import afnio
>>> import afnio.cognitive.functional as F
>>> a = afnio.Variable("Hello,", requires_grad=True)
>>> b = afnio.Variable("world!", requires_grad=True)
>>> c = F.sum([a, b])
>>> assert isinstance(c.grad_fn, afnio.autodiff.graph.Node)
>>> print(c.grad_fn.name())
SumBackward0
Source code in afnio/autodiff/graph.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def name(self) -> str:
    """
    Returns the name of the backward node.

    Returns:
        The name of the backward node, which is typically the name of the forward operation that produced the output associated with this node.

    Examples:
        >>> import afnio
        >>> import afnio.cognitive.functional as F
        >>> a = afnio.Variable("Hello,", requires_grad=True)
        >>> b = afnio.Variable("world!", requires_grad=True)
        >>> c = F.sum([a, b])
        >>> assert isinstance(c.grad_fn, afnio.autodiff.graph.Node)
        >>> print(c.grad_fn.name())
        SumBackward0
    """  # noqa: E501
    return self._name

afnio.get_backward_model_client()

Retrieve the global model client singleton.

This is useful for tracking token usage across the entire backward pass, as all backward operations will use this global client instance.

Returns:

Type Description
ModelClientSingleton

The global model client singleton instance.

Raises:

Type Description
RuntimeError

If no model client is set globally.

Source code in afnio/_model_client.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
def get_backward_model_client() -> ModelClientSingleton:
    """
    Retrieve the global model client singleton.

    This is useful for tracking token usage across the entire backward pass,
    as all backward operations will use this global client instance.

    Returns:
        The global model client singleton instance.

    Raises:
        RuntimeError: If no model client is set globally.
    """
    if _model_singleton._client is None:
        raise RuntimeError(
            "No global model client set for backward pass. "
            "Call `set_backward_model_client` to define one."
        )
    return _model_singleton

afnio.set_backward_model_client(model_path='openai/gpt-4o', client_args=None, completion_args=None)

Set the global model client for backward operations.

Parameters:

Name Type Description Default
model_path str

Path in the format provider/model_name (e.g., "openai/gpt-4o").

'openai/gpt-4o'
client_args dict[str, Any] | None

Arguments to initialize the model client such as:

  • api_key (str): The client API key.
  • organization (str): The organization to bill.
  • base_url (str): The model base endpoint URL (useful when models are behind a proxy).
  • etc.
None
completion_args dict[str, Any] | None

Arguments to pass to achat() during usage such as:

  • model (str): The model to use (e.g., gpt-4o).
  • temperature (float): Amount of randomness injected into the response.
  • max_completion_tokens (int): Maximum number of tokens to generate.
  • etc.
None
Note

For a complete list of supported client_args and completion_args for each model, refer to the respective API documentation.

Source code in afnio/_model_client.py
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
def set_backward_model_client(
    model_path: str = "openai/gpt-4o",
    client_args: Optional[Dict[str, Any]] = None,
    completion_args: Optional[Dict[str, Any]] = None,
):
    """
    Set the global model client for backward operations.

    Args:
        model_path: Path in the format `provider/model_name`
            (e.g., `"openai/gpt-4o"`).
        client_args: Arguments to initialize the model client such as:

            - `api_key` (str): The client API key.
            - `organization` (str): The organization to bill.
            - `base_url` (str): The model base endpoint URL (useful when models are
              behind a proxy).
            - etc.
        completion_args: Arguments to pass to `achat()` during usage such as:

            - `model` (str): The model to use (e.g., `gpt-4o`).
            - `temperature` (float): Amount of randomness injected into the response.
            - `max_completion_tokens` (int): Maximum number of tokens to generate.
            - etc.

    Note:
        For a complete list of supported `client_args` and `completion_args`
        for each model, refer to the respective API documentation.
    """
    try:
        provider, model = model_path.split("/", 1)
    except ValueError:
        raise ValueError("`model_path` must be in the format 'provider/model'")

    # Ensure client_args is a dict
    if client_args is None:
        client_args = {}

    # Set api_key to value in client_args if present, else from env or None
    if provider == "openai":
        client_args["api_key"] = client_args.get("api_key", os.getenv("OPENAI_API_KEY"))
    else:
        raise ValueError(f"Unsupported provider: {provider}.")

    _model_singleton._initialize(provider, model, client_args, completion_args)

afnio.is_grad_enabled()

Check whether grad mode is currently enabled.

Returns:

Type Description
bool

True if grad mode is currently enabled, False otherwise.

Source code in afnio/autodiff/grad_mode.py
 9
10
11
12
13
14
15
16
def is_grad_enabled() -> bool:
    """
    Check whether grad mode is currently enabled.

    Returns:
        `True` if grad mode is currently enabled, `False` otherwise.
    """
    return getattr(_grad_enabled, "enabled", True)

afnio.no_grad()

Context manager that disables gradient calculation. All operations within this block will not track gradients, making them more memory-efficient.

Disabling gradient calculation is useful for inference, when you are sure that you will not call Variable.backward(). It will reduce memory consumption for computations that would otherwise have requires_grad=True.

In this mode, the result of every computation will have requires_grad=False, even when the inputs have requires_grad=True. There is an exception! All factory functions, or functions that create a new Variable and take a requires_grad kwarg, will NOT be affected by this mode.

This context manager is thread local; it will not affect computation in other threads.

Also functions as a decorator.

Examples:

>>> x = afnio.Variable("abc", role="variable", requires_grad=True)
>>> with afnio.no_grad():
...     y = x + x
>>> y.requires_grad
False
>>> @afnio.no_grad()
... def doubler(x):
...     return x + x
>>> z = doubler(x)
>>> z.requires_grad
False
>>> @afnio.no_grad()
... def tripler(x):
...     return x + x + x
>>> z = tripler(x)
>>> z.requires_grad
False
>>> # factory function exception
>>> with afnio.no_grad():
...     a = afnio.cognitive.Parameter("xyz")
>>> a.requires_grad
True
Source code in afnio/autodiff/grad_mode.py
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
@contextmanager
def no_grad():
    """
    Context manager that disables gradient calculation. All operations within this block
    will not track gradients, making them more memory-efficient.

    Disabling gradient calculation is useful for inference, when you are sure
    that you will not call [`Variable.backward()`][afnio.Variable.backward]. It will
    reduce memory consumption for computations that would otherwise have
    `requires_grad=True`.

    In this mode, the result of every computation will have
    `requires_grad=False`, even when the inputs have `requires_grad=True`.
    There is an exception! All factory functions, or functions that create
    a new Variable and take a requires_grad kwarg, will NOT be affected by
    this mode.

    This context manager is thread local; it will not affect computation
    in other threads.

    Also functions as a decorator.

    Examples:
        >>> x = afnio.Variable("abc", role="variable", requires_grad=True)
        >>> with afnio.no_grad():
        ...     y = x + x
        >>> y.requires_grad
        False
        >>> @afnio.no_grad()
        ... def doubler(x):
        ...     return x + x
        >>> z = doubler(x)
        >>> z.requires_grad
        False
        >>> @afnio.no_grad()
        ... def tripler(x):
        ...     return x + x + x
        >>> z = tripler(x)
        >>> z.requires_grad
        False
        >>> # factory function exception
        >>> with afnio.no_grad():
        ...     a = afnio.cognitive.Parameter("xyz")
        >>> a.requires_grad
        True
    """
    previous_state = is_grad_enabled()  # Store the current state
    set_grad_enabled(False)  # Disable gradients
    try:
        yield  # Execute the block
    finally:
        set_grad_enabled(previous_state)  # Restore the original state

afnio.set_grad_enabled(mode)

Set the global state of gradient calculation on or off.

set_grad_enabled will enable or disable gradients based on its argument mode.

Parameters:

Name Type Description Default
mode bool

If True, enables gradient calculation. If False, disables it.

required

Examples:

>>> x = afnio.Variable("Hello", requires_grad=True)
>>> _ = afnio.set_grad_enabled(True)
>>> y = x + x
>>> y.requires_grad
True
>>> _ = afnio.set_grad_enabled(False)
>>> y = x + x
>>> y.requires_grad
False
Source code in afnio/autodiff/grad_mode.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def set_grad_enabled(mode: bool):
    """
    Set the global state of gradient calculation on or off.

    `set_grad_enabled` will enable or disable gradients based on its argument `mode`.

    Args:
        mode: If `True`, enables gradient calculation. If `False`, disables it.

    Examples:
        >>> x = afnio.Variable("Hello", requires_grad=True)
        >>> _ = afnio.set_grad_enabled(True)
        >>> y = x + x
        >>> y.requires_grad
        True
        >>> _ = afnio.set_grad_enabled(False)
        >>> y = x + x
        >>> y.requires_grad
        False
    """
    _grad_enabled.enabled = mode

afnio.load(f)

Loads an object from a disk file using zip compression and pickle serialization.

Parameters:

Name Type Description Default
f str | PathLike | BinaryIO | IO[bytes]

A file-like object (must implement read) or a string or os.PathLike object containing a file name.

required

Returns:

Type Description
Any

The deserialized object.

Examples:

>>> # Load from file
>>> obj = afnio.load('model.hf')
>>> # Load from io.BytesIO buffer
>>> buffer = io.BytesIO()
>>> obj = afnio.load(buffer)
Source code in afnio/serialization.py
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
def load(f: FILE_LIKE) -> Any:
    """
    Loads an object from a disk file using zip compression and pickle serialization.

    Args:
        f (Union[str, os.PathLike, BinaryIO, IO[bytes]]): A file-like object (must
            implement `read`) or a string or `os.PathLike` object containing
            a file name.

    Returns:
        The deserialized object.

    Examples:
        >>> # Load from file
        >>> obj = afnio.load('model.hf')
        >>> # Load from io.BytesIO buffer
        >>> buffer = io.BytesIO()
        >>> obj = afnio.load(buffer)
    """
    with _open_zipfile_reader(f) as zip_reader:
        if "data.pkl" not in zip_reader.namelist():
            raise RuntimeError(
                "Missing 'data.pkl' in archive. File might be corrupted."
            )

        # Read the serialized object
        with zip_reader.open("data.pkl", "r") as f:
            obj = pickle.load(f)

    return obj

afnio.save(obj, f, pickle_protocol=DEFAULT_PROTOCOL)

Saves an object to a disk file using zip compression and pickle serialization.

Parameters:

Name Type Description Default
obj object

The object to be saved.

required
f str | PathLike | BinaryIO | IO[bytes]

A file-like object (must implement write/flush) or a string or os.PathLike object containing a file name.

required
pickle_protocol int

Pickle protocol version.

DEFAULT_PROTOCOL
Note

A common Afnio convention is to save variables using .hf file extension.

The .hf extension is a naming convention inspired by the chemical symbol for Hafnium (Hf). "Afnio" is the Italian word for Hafnium.

Examples:

>>> # Save to file
>>> x = afnio.Variable(data="You are a doctor.", role="system prompt")
>>> afnio.save(x, 'variable.hf')
>>> # Save to io.BytesIO buffer
>>> buffer = io.BytesIO()
>>> afnio.save(x, buffer)
Source code in afnio/serialization.py
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
def save(
    obj: object,
    f: FILE_LIKE,
    pickle_protocol: int = DEFAULT_PROTOCOL,
) -> None:
    """
    Saves an object to a disk file using zip compression and pickle serialization.

    Args:
        obj: The object to be saved.
        f (Union[str, os.PathLike, BinaryIO, IO[bytes]]): A file-like object (must
            implement write/flush) or a string or `os.PathLike` object containing
            a file name.
        pickle_protocol: Pickle protocol version.

    Note:
        A common Afnio convention is to save variables using `.hf` file extension.

        The `.hf` extension is a naming convention inspired by the chemical symbol
        for [Hafnium (Hf)](https://en.wikipedia.org/wiki/Hafnium). "Afnio" is the
        Italian word for Hafnium.

    Examples:
        >>> # Save to file
        >>> x = afnio.Variable(data="You are a doctor.", role="system prompt")
        >>> afnio.save(x, 'variable.hf')
        >>> # Save to io.BytesIO buffer
        >>> buffer = io.BytesIO()
        >>> afnio.save(x, buffer)
    """
    _check_save_filelike(f)

    with _open_zipfile_writer(f) as opened_zipfile:
        _save(
            obj,
            opened_zipfile,
            pickle_protocol,
        )
        return