"""Container component for composing multiple components, such as Sequentialand ComponentList.This design draws inspiration from PyTorch’s modularcontainer patterns, including `nn.Sequential` and `nn.ModuleList`. The`Container` component allows for grouping several components into one, enablingflexible and reusable model architectures.Design Motivation:-------------------This implementation follows the same principles as PyTorch’s component-baseddesign, encouraging modularity, reusability, and extensibility. The `Container`component provides an easy way to manage multiple layers or other components,while ensuring that their parameters are properly registered and updated duringtraining.Credits:---------The design of this component takes inspiration from the PyTorch project(https://pytorch.org). PyTorch is an open-source deep learning framework,licensed under a BSD-style license. Although this code is not part of theofficial PyTorch library, it mirrors the same design principles.For more details on PyTorch’s licensing, refer to:https://github.com/pytorch/pytorch/blob/main/LICENSEUsage Example:-------------- class MyModule(nn.Module): def __init__(self): super().__init__() self.model = nn.Sequential( nn.Conv2d(1,20,5), nn.ReLU(), nn.Conv2d(20,64,5), nn.ReLU() ) self.linears = nn.ModuleList([nn.Linear(10, 10) for i in range(10)]) def forward(self, x): # ModuleList can act as an iterable, or be indexed using ints for i, l in enumerate(self.linears): x = self.linears[i // 2](x) + l(x) return x"""fromcollectionsimportOrderedDict,abcascontainer_abcsimportoperatorfromitertoolsimportislice,chainfromtypingimportTypeVar,Dict,Union,Iterable,Iterator,Any,overload,Optionalfromadalflow.core.componentimportComponentT=TypeVar("T",bound=Component)__all__=["Sequential","ComponentList"]
[docs]classSequential(Component):__doc__=r"""A sequential container. Adapted from PyTorch's ``nn.Sequential``. Components will be added to it in the order they are passed to the constructor. Alternatively, an ``OrderedDict`` of components can be passed in. It "chains" outputs of the previous component to the input of the next component sequentially. Output of the previous component is input to the next component as positional argument. Benefits of using Sequential: 1. Convenient for data pipeline that often consists of multiple components. This allow users to encapsulate the pipeline in a single component. Examples: Without Sequential: .. code-block:: python class AddAB(Component): def call(self, a: int, b: int) -> int: return a + b class MultiplyByTwo(Component): def call(self, input: int) -> int: return input * 2 class DivideByThree(Component): def call(self, input: int) -> int: return input / 3 # Manually chaining the components add_a_b = AddAB() multiply_by_two = MultiplyByTwo() divide_by_three = DivideByThree() result = divide_by_three(multiply_by_two(add_a_b(2, 3))) With Sequential: .. code-block:: python seq = Sequential(AddAB(), MultiplyByTwo(), DivideByThree()) result = seq(2, 3) .. note:: Only the first component can receive arbitrary positional and keyword arguments. The rest of the components should have a single positional argument as input and have it to be exactly the same type as the output of the previous component. 2. Apply a transformation or operation (like training, evaluation, or serialization) to the Sequential object, it automatically applies that operation to each component it contains. This can be useful for In-context learning training. Examples: 1. Use positional arguments: >>> seq = Sequential(component1, component2) 2. Add components: >>> seq.append(component4) 3. Get a component: >>> seq[0] 4. Delete a component: >>> del seq[0] 5. Iterate over components: >>> for component in seq: >>> print(component) 6. Add two Sequentials: >>> seq1 = Sequential(component1, component2) >>> seq2 = Sequential(component3, component4) >>> seq3 = seq1 + seq2 7. Use OrderedDict: >>> seq = Sequential(OrderedDict({"component1": component1, "component2": component2})) 8. Index OrderDict: >>> seq = Sequential(OrderedDict({"component1": component1, "component2": component2})) >>> seq["component1"] # or >>> seq[0] 9. Call with a single argument as input: >>> seq = Sequential(component1, component2) >>> result = seq.call(2) 10. Call with multiple arguments as input: >>> seq = Sequential(component1, component2) >>> result = seq.call(2, 3) """_components:Dict[str,Component]=OrderedDict()# type: ignore[assignment]@overloaddef__init__(self,*args:Component)->None:...@overloaddef__init__(self,arg:"OrderedDict[str, Component]")->None:...def__init__(self,*args):super().__init__()iflen(args)==1andisinstance(args[0],OrderedDict):forkey,componentinargs[0].items():self.add_component(key,component)else:foridx,componentinenumerate(args):self.add_component(str(idx),component)def_get_item_by_idx(self,iterator:Iterator[Component],idx:int)->Component:"""Get the idx-th item of the iterator."""size=len(self)idx=operator.index(idx)ifnot-size<=idx<size:raiseIndexError(f"index {idx} is out of range")idx%=sizereturnnext(islice(iterator,idx,None))def__getitem__(self,idx:Union[slice,int,str])->Union["Sequential",Component]:"""Get the idx-th and by-key component of the Sequential."""ifisinstance(idx,slice):returnself.__class__(OrderedDict(list(self._components.items())[idx]))elifisinstance(idx,str):returnself._components[idx]else:returnself._get_item_by_idx(iter(self._components.values()),idx)def__setitem__(self,idx:Union[int,str],component:Component)->None:"""Set the idx-th component of the Sequential."""ifisinstance(idx,str):self._components[idx]=componentelse:# key: str = self._get_item_by_idx(iter(self._components.keys()), idx)# self._components[key] = componentkey_list=list(self._components.keys())key=key_list[idx]self._components[key]=componentdef__delitem__(self,idx:Union[slice,int,str])->None:"""Delete the idx-th component of the Sequential."""ifisinstance(idx,slice):forkeyinlist(self._components.keys())[idx]:delattr(self,key)elifisinstance(idx,str):delself._components[idx]else:# key = self._get_item_by_idx(iter(self._components.keys()), idx)key_list=list(self._components.keys())key=key_list[idx]delattr(self,key)# Reordering is needed if numerical keys are used to keep the sequenceself._components=OrderedDict((str(i),comp)fori,compinenumerate(self._components.values()))def__iter__(self)->Iterator[Component]:r"""Iterates over the components of the Sequential. Examples: 1. Iterate over the components: .. code-block:: python for component in seq: print(component) """returniter(self._components.values())def__len__(self)->int:returnlen(self._components)def__add__(self,other)->"Sequential":r"""Adds two Sequentials. Creating a new Sequential with components of both the Sequentials. Examples: 1. Add two Sequentials: .. code-block:: python seq1 = Sequential(component1, component2) seq2 = Sequential(component3, component4) seq3 = seq1 + seq2 """ifisinstance(other,Sequential):ret=Sequential()forlayerinself:ret.append(layer)forlayerinother:ret.append(layer)returnretelse:raiseValueError("add operator supports only objects "f"of Sequential class, but {str(type(other))} is given.")def__iadd__(self,other)->"Sequential":r"""Inplace add two Sequentials. Adding components of the other Sequential to the current Sequential. Examples: 1. Inplace add two Sequentials: .. code-block:: python seq1 = Sequential(component1, component2) seq2 = Sequential(component3, component4) seq1 += seq2 """ifnotisinstance(other,Sequential):raiseValueError("add operator supports only objects "f"of Sequential class, but {str(type(other))} is given.")forlayerinother:self.append(layer)returnself@overloaddefcall(self,input:Any)->object:...@overloaddefcall(self,*args:Any,**kwargs:Any)->object:...
[docs]asyncdefacall(self,*args:Any,**kwargs:Any)->object:r"""When you for loop or multiple await calls inside each component, use acall method can potentially speed up the execution."""iflen(args)==1andnotkwargs:input=args[0]forcomponentinself._components.values():input=awaitcomponent(input)returninputelse:forcomponentinself._components.values():result=awaitcomponent(*args,**kwargs)if(isinstance(result,tuple)andlen(result)==2andisinstance(result[1],dict)):args,kwargs=resultelse:args=(result,)kwargs={}returnargs[0]iflen(args)==1else(args,kwargs)
[docs]defappend(self,component:Component)->"Sequential":r"""Appends a component to the end of the Sequential."""idx=len(self._components)self.add_component(str(idx),component)returnself
[docs]definsert(self,idx:int,component:Component)->None:r"""Inserts a component at a given index in the Sequential."""ifnotisinstance(component,Component):raiseTypeError(f"component should be an instance of Component, but got {type(component)}")n=len(self._components)ifnot(-n<=idx<=n):raiseIndexError(f"index {idx} is out of range for Sequential with length {len(self._components)}")ifidx<0:idx+=nforiinrange(n,idx,-1):self._components[str(i)]=self._components[str(i-1)]self._components[str(idx)]=component
[docs]defextend(self,components:Iterable[Component])->"Sequential":r"""Extends the Sequential with components from an iterable."""forcomponentincomponents:self.append(component)returnself
def_addindent(s_:str,numSpaces:int):s=s_.split("\n")# don't do anything for single-line stuffiflen(s)==1:returns_first=s.pop(0)s=[(numSpaces*" ")+lineforlineins]s="\n".join(s)s=first+"\n"+sreturns
[docs]classComponentList(Component):__doc__=r"""Holds subcomponents in a list. :class:`adalflow.core.ComponentList` can be indexed like a regular Python list, but the components it holds are properly registered, and will be visible by all :class:`adalflow.core.Component` methods. Args: components (iterable, optional): an iterable of components to add Examples: .. code-block:: python # Example of how to use ComponentList class MyComponents(Component): def __init__(self): super().__init__() self.llms = ComponentList([adal.Generator() for i in range(10)]) def forward(self, x): for layer in self.layers: x = layer(x) return x """_components:Dict[str,Component]=OrderedDict()def__init__(self,components:Optional[Iterable[Component]]=None)->None:super().__init__()ifcomponentsisnotNone:self+=componentsdef_get_abs_string_index(self,idx):"""Get the absolute index as a string."""idx=operator.index(idx)ifnot(-len(self)<=idx<len(self)):raiseIndexError(f"index {idx} is out of range")ifidx<0:idx+=len(self)returnstr(idx)def__getitem__(self,idx:Union[int,slice])->Union[Component,"ComponentList"]:"""Retrieve a component or a slice of components."""ifisinstance(idx,slice):returnself.__class__(list(self._components.values())[idx])else:returnself._components[self._get_abs_string_index(idx)]def__setitem__(self,idx:int,component:Component)->None:"""Set a component at the given index."""idx=self._get_abs_string_index(idx)returnsetattr(self,str(idx),component)def__delitem__(self,idx:Union[int,slice])->None:"""Delete a component or a slice of components."""ifisinstance(idx,slice):forkinrange(len(self._components))[idx]:delattr(self,str(k))else:delattr(self,self._get_abs_string_index(idx))# To preserve numbering, self._components is being reconstructed with modules after deletionstr_indices=[str(i)foriinrange(len(self._components))]self._components=OrderedDict(list(zip(str_indices,self._components.values())))def__len__(self)->int:"""Return the number of components."""returnlen(self._components)def__iter__(self)->Iterator[Component]:"""Iterate over the components."""returniter(self._components.values())def__iadd__(self,components:Iterable[Component])->"ComponentList":"""Add multiple components using the `+=` operator."""returnself.extend(components)def__add__(self,other:Iterable[Component])->"ComponentList":"""Concatenate two ComponentLists."""combined=ComponentList()fori,componentinenumerate(chain(self,other)):combined.add_component(str(i),component)returncombineddef__repr__(self):"""Return a custom repr for ModuleList that compresses repeated module representations."""list_of_reprs=[repr(item)foriteminself]iflen(list_of_reprs)==0:returnself._get_name()+"()"start_end_indices=[[0,0]]repeated_blocks=[list_of_reprs[0]]fori,rinenumerate(list_of_reprs[1:],1):ifr==repeated_blocks[-1]:start_end_indices[-1][1]+=1continuestart_end_indices.append([i,i])repeated_blocks.append(r)lines=[]main_str=self._get_name()+"("for(start_id,end_id),binzip(start_end_indices,repeated_blocks):local_repr=f"({start_id}): {b}"# default reprifstart_id!=end_id:n=end_id-start_id+1local_repr=f"({start_id}-{end_id}): {n} x {b}"local_repr=_addindent(local_repr,2)lines.append(local_repr)main_str+="\n "+"\n ".join(lines)+"\n"main_str+=")"returnmain_strdef__dir__(self):keys=super().__dir__()keys=[keyforkeyinkeysifnotkey.isdigit()]returnkeys
[docs]definsert(self,index:int,component:Component)->None:"""Insert a component at the specified index."""foriinrange(len(self._components),index,-1):self._components[str(i)]=self._components[str(i-1)]self._components[str(index)]=component
[docs]defpop(self,index:Union[int,slice])->Component:"""Remove and return a component at the given index."""component=self[index]delself[index]returncomponent
[docs]defappend(self,component:Component)->"ComponentList":"""Append a component to the list."""# self._components[str(len(self))] = componentself.add_component(str(len(self)),component)returnself
[docs]defextend(self,components:Iterable[Component])->"ComponentList":"""Extend the list by appending multiple components."""# for component in components:# self.append(component)# return selfifnotisinstance(components,container_abcs.Iterable):raiseTypeError("ModuleList.extend should be called with an ""iterable, but got "+type(components).__name__)offset=len(self)fori,componentinenumerate(components):self.add_component(str(offset+i),component)returnself
# TODO: need to do the same to ParameterList and ParameterDict, ModuleDict