| 9 | from typing import Union |
| 10 | |
| 11 | class Variable: |
| 12 | def __init__( |
| 13 | self, |
| 14 | value: Union[str, bytes] = "", |
| 15 | image_path: str = "", |
| 16 | predecessors: List['Variable']=None, |
| 17 | requires_grad: bool=True, |
| 18 | *, |
| 19 | role_description: str): |
| 20 | """The main thing. Nodes in the computation graph. Really the heart and soul of textgrad. |
| 21 | |
| 22 | :param value: The string value of this variable, defaults to "". In the future, we'll go multimodal, for sure! |
| 23 | :type value: str or bytes, optional |
| 24 | :param image_path: The path to the image file, defaults to "". If present we will read from disk or download the image. |
| 25 | :type image_path: str, optional |
| 26 | :param predecessors: predecessors of this variable in the computation graph, defaults to None. Here, for instance, if we have a prompt -> response through an LLM call, we'd call the prompt the predecessor, and the response the successor. |
| 27 | :type predecessors: List[Variable], optional |
| 28 | :param requires_grad: Whether this variable requires a gradient, defaults to True. If False, we'll not compute the gradients on this variable. |
| 29 | :type requires_grad: bool, optional |
| 30 | :param role_description: The role of this variable. We find that this has a huge impact on the optimization performance, and being specific often helps quite a bit! |
| 31 | :type role_description: str |
| 32 | """ |
| 33 | |
| 34 | if predecessors is None: |
| 35 | predecessors = [] |
| 36 | |
| 37 | _predecessor_requires_grad = [v for v in predecessors if v.requires_grad] |
| 38 | |
| 39 | if (not requires_grad) and (len(_predecessor_requires_grad) > 0): |
| 40 | raise Exception("If the variable does not require grad, none of its predecessors should require grad." |
| 41 | f"In this case, following predecessors require grad: {_predecessor_requires_grad}") |
| 42 | |
| 43 | # Handle numpy types by converting them to native Python types |
| 44 | try: |
| 45 | import numpy as np |
| 46 | if isinstance(value, np.integer): |
| 47 | value = int(value) |
| 48 | elif isinstance(value, np.floating): |
| 49 | value = float(value) |
| 50 | except ImportError: |
| 51 | pass # numpy not available, continue without conversion |
| 52 | |
| 53 | assert type(value) in [str, bytes, int], "Value must be a string, int, or image (bytes). Got: {}".format(type(value)) |
| 54 | if isinstance(value, int): |
| 55 | value = str(value) |
| 56 | # We'll currently let "empty variables" slide, but we'll need to handle this better in the future. |
| 57 | # if value == "" and image_path == "": |
| 58 | # raise ValueError("Please provide a value or an image path for the variable") |
| 59 | if value != "" and image_path != "": |
| 60 | raise ValueError("Please provide either a value or an image path for the variable, not both.") |
| 61 | |
| 62 | if image_path != "": |
| 63 | if is_valid_url(image_path): |
| 64 | self.value = httpx.get(image_path).content |
| 65 | else: |
| 66 | with open(image_path, 'rb') as file: |
| 67 | self.value = file.read() |
| 68 | else: |
no outgoing calls