zodiac.graph

  1#  # # <!-- // /*  SPDX-License-Identifier: MPL-2.0*/ -->
  2#  # # <!-- // /*  d a r k s h a p e s */ -->
  3
  4
  5import sys
  6import os
  7import networkx as nx
  8from typing import Optional
  9from nnll.monitor.file import dbug, dbuq
 10from zodiac.providers.pools import register_models  # leaving here for mocking
 11
 12sys.path.append(os.getcwd())
 13nfo = print
 14
 15
 16class IntentProcessor:
 17    intent_graph: Optional[dict[nx.Graph]] = None
 18    coord_path: Optional[list[str]] = None
 19    registry_entries: Optional[list[dict[dict]]] = None
 20    models: Optional[list[tuple[str]]] = None
 21    weight_idx: Optional[list[str]] = None
 22    # additional_model_names: dict = None
 23
 24    def __init__(self, intent_graph: nx.MultiDiGraph = nx.MultiDiGraph()) -> None:
 25        """
 26        Create instance of graph processor & initialize objectieves for tracing paths\n
 27        :param nx_graph:Preassembled graph of models to substitute, default uses nx.MultiDiGraph()
 28
 29        ========================================================\n
 30        ### GIVEN\n
 31        A : The list of `VALID CONVERSIONS` contains all of Zodiac's supported generative modalities\n
 32        B : The graph is populated directly from the contents of the list in A\n
 33        Thus: All possible node start and end points listed in A are included in graph B.\n
 34        Therefore : It is impossible to call a node that does not exist.\n
 35        """
 36        from zodiac.providers.constants import VALID_CONVERSIONS
 37
 38        self.intent_graph = intent_graph
 39        self.intent_graph.add_nodes_from(VALID_CONVERSIONS)
 40
 41    async def calc_graph(self, registry_entries: Optional[list] = None) -> None:
 42        """Generate graph of coordinate pairs from valid conversions\n
 43        Model libraries are auto-detected from cache loading\n
 44        :param registry_data: Registry function or method of calling registry, defaults to
 45        :return: Graph modeling all current ML/AI tasks appended with model data
 46
 47        ========================================================\n
 48        ### GIVEN\n
 49        A : The set of all models M on the executing system\n
 50        B : P is the randomly distributed set of start and end points required to graph M\n
 51        Thus: Because of the randomness of B, the set P is unlikely to construct a complete graph attached all available points.\n
 52        Therefore : While we can trust a node exists, we **CANNOT** trust the system has an edge to reach it\n
 53        """
 54        # import asyncio
 55
 56        if not registry_entries:
 57            registry_entries = await register_models()
 58        nfo("Building graph...")
 59
 60        if registry_entries is None:
 61            nfo("Registry error, graph attributes not applied.")
 62        elif len(self.intent_graph.edges) > 0:
 63            nfo("Edges already calculated")
 64            return self.intent_graph
 65        else:
 66            for model in registry_entries:
 67                try:
 68                    self.intent_graph.add_edges_from(model.available_tasks, entry=model, weight=1.0)
 69                except AttributeError as error_log:
 70                    dbug(error_log)
 71                    nfo("Error: Registry initialized but not populated with data. Graph could not create edges.")
 72
 73        nfo("Complete {self.intent_graph}")
 74        return self.intent_graph
 75
 76    def set_path(self, mode_in: str, mode_out: str) -> None:
 77        """Find a valid path from current state (mode_in) to designated state (mode_out)\n
 78        :param mode_in: Input prompt type or starting state/states
 79        :type mode_in: str
 80        :param mode_out: The user-selected ending-state
 81        :type mode_out: str
 82        """
 83
 84        if nx.has_path(self.intent_graph, mode_in, mode_out):  # Ensure path exists (otherwise 'bidirectional' may loop infinitely)
 85            # Self loops in the multidirected graph complete themselves
 86            # In practice, this means often the same model can be used to compute prompt input and response output
 87            # Unfortunately, this doesn't always work in all modalities, ex. Image to Image.
 88            # This condition is meant to solve the case of non-text self-loop edge being an incomplete transformation
 89
 90            if mode_in == mode_out and mode_in != "text":  # Its not a great solution, but it works for the moment
 91                orig_mode_out = mode_out
 92                mode_out = "text"
 93                self.coord_path = nx.bidirectional_shortest_path(self.intent_graph, mode_in, mode_out)
 94                self.coord_path.append(orig_mode_out)
 95            else:
 96                self.coord_path = nx.bidirectional_shortest_path(self.intent_graph, mode_in, mode_out)
 97                if len(self.coord_path) == 1:
 98                    self.coord_path.append(mode_out)  # this behaviour likely to change in future
 99
100        else:
101            nfo("No Path available...\n")
102
103    def set_registry_entries(self) -> None:
104        """Populate models list for text fields
105        Check if model has been adjusted, if so adjust list
106        1.0 weight bottom, <1.0 weight top"""
107
108        try:
109            self.registry_entries = self.pull_path_entries(self.intent_graph, self.coord_path)
110        except KeyError as error_log:
111            dbug(error_log)
112            return ["", ""]
113        idx = 0
114        self.models = []
115
116        if self.registry_entries:
117            for edge, registry in enumerate(self.registry_entries):
118                model = registry["entry"].model
119                dbuq(f"node {edge}")
120                adj_model = (os.path.basename(model), edge)
121                self.models.append(adj_model)
122            self.weight_idx = self.weight_idx or []
123            for model in self.weight_idx:
124                if model in self.models:
125                    self.models.remove(model)
126                    adj_model = (f"*{model[0]}", model[1])
127                    self.models.insert(idx, adj_model)
128                    idx += 1
129
130    def edit_weight(self, edge_number: str, mode_in: str, mode_out: str) -> None:
131        """Determine entry edge, determine index, then adjust weight\n
132        :param edge_number: Text pattern from `models` class attribute to identify the model by
133        :param mode_in: The conversion type, representing a source graph node
134        :param mode_out: The target type, , representing a source graph node
135        :raises ValueError: No models fit the request
136        """
137
138        self.weight_idx = self.weight_idx or []
139
140        try:
141            if not nx.has_path(self.intent_graph, mode_in, mode_out):
142                raise KeyError()
143            model = self.intent_graph[mode_in][mode_out][edge_number]["entry"].model
144        except KeyError as error_log:
145            nfo(
146                f"Failed to adjust weight of '{edge_number}' within registry contents \
147                '{self.intent_graph} {mode_in} {mode_out}'. Model or registry entry not found. "
148            )
149            dbug(error_log)
150            return self.set_registry_entries()
151
152        weight = self.intent_graph[mode_in][mode_out][edge_number]["weight"]
153        item = (os.path.basename(model), edge_number)
154        nfo(f" model : {model}  weight: {weight} ")
155
156        if weight < 1.0:
157            self.intent_graph[mode_in][mode_out][edge_number]["weight"] = round(weight + 0.1, 1)
158            self.models = [((f"*{os.path.basename(model)}", edge_number))]
159            if item in self.weight_idx:
160                self.weight_idx.remove(item)
161        else:
162            self.intent_graph[mode_in][mode_out][edge_number]["weight"] = round(weight - 0.1, 1)
163            self.weight_idx.append(item)
164        self.set_registry_entries()
165
166    def pull_path_entries(self, nx_graph: nx.Graph, traced_path: list[tuple]) -> None:
167        """Create operating instructions from user input
168        Trace the next hop along the path, collect all compatible models
169        Set current model based on weight and next available"""
170
171        registry_entries = []
172        if traced_path is not None and nx.has_path(nx_graph, traced_path[0], traced_path[1]):
173            registry_entries = [  # ruff : noqa
174                nx_graph[traced_path[index]][traced_path[index + 1]][hop]  #
175                for index in range(len(traced_path) - 1)  #
176                for hop in nx_graph[traced_path[index]][traced_path[index + 1]]  #
177            ]
178        return registry_entries
def nfo(*args, sep=' ', end='\n', file=None, flush=False):

Prints the values to a stream, or to sys.stdout by default.

sep string inserted between values, default a space. end string appended after the last value, default a newline. file a file-like object (stream); defaults to the current sys.stdout. flush whether to forcibly flush the stream.

class IntentProcessor:
 17class IntentProcessor:
 18    intent_graph: Optional[dict[nx.Graph]] = None
 19    coord_path: Optional[list[str]] = None
 20    registry_entries: Optional[list[dict[dict]]] = None
 21    models: Optional[list[tuple[str]]] = None
 22    weight_idx: Optional[list[str]] = None
 23    # additional_model_names: dict = None
 24
 25    def __init__(self, intent_graph: nx.MultiDiGraph = nx.MultiDiGraph()) -> None:
 26        """
 27        Create instance of graph processor & initialize objectieves for tracing paths\n
 28        :param nx_graph:Preassembled graph of models to substitute, default uses nx.MultiDiGraph()
 29
 30        ========================================================\n
 31        ### GIVEN\n
 32        A : The list of `VALID CONVERSIONS` contains all of Zodiac's supported generative modalities\n
 33        B : The graph is populated directly from the contents of the list in A\n
 34        Thus: All possible node start and end points listed in A are included in graph B.\n
 35        Therefore : It is impossible to call a node that does not exist.\n
 36        """
 37        from zodiac.providers.constants import VALID_CONVERSIONS
 38
 39        self.intent_graph = intent_graph
 40        self.intent_graph.add_nodes_from(VALID_CONVERSIONS)
 41
 42    async def calc_graph(self, registry_entries: Optional[list] = None) -> None:
 43        """Generate graph of coordinate pairs from valid conversions\n
 44        Model libraries are auto-detected from cache loading\n
 45        :param registry_data: Registry function or method of calling registry, defaults to
 46        :return: Graph modeling all current ML/AI tasks appended with model data
 47
 48        ========================================================\n
 49        ### GIVEN\n
 50        A : The set of all models M on the executing system\n
 51        B : P is the randomly distributed set of start and end points required to graph M\n
 52        Thus: Because of the randomness of B, the set P is unlikely to construct a complete graph attached all available points.\n
 53        Therefore : While we can trust a node exists, we **CANNOT** trust the system has an edge to reach it\n
 54        """
 55        # import asyncio
 56
 57        if not registry_entries:
 58            registry_entries = await register_models()
 59        nfo("Building graph...")
 60
 61        if registry_entries is None:
 62            nfo("Registry error, graph attributes not applied.")
 63        elif len(self.intent_graph.edges) > 0:
 64            nfo("Edges already calculated")
 65            return self.intent_graph
 66        else:
 67            for model in registry_entries:
 68                try:
 69                    self.intent_graph.add_edges_from(model.available_tasks, entry=model, weight=1.0)
 70                except AttributeError as error_log:
 71                    dbug(error_log)
 72                    nfo("Error: Registry initialized but not populated with data. Graph could not create edges.")
 73
 74        nfo("Complete {self.intent_graph}")
 75        return self.intent_graph
 76
 77    def set_path(self, mode_in: str, mode_out: str) -> None:
 78        """Find a valid path from current state (mode_in) to designated state (mode_out)\n
 79        :param mode_in: Input prompt type or starting state/states
 80        :type mode_in: str
 81        :param mode_out: The user-selected ending-state
 82        :type mode_out: str
 83        """
 84
 85        if nx.has_path(self.intent_graph, mode_in, mode_out):  # Ensure path exists (otherwise 'bidirectional' may loop infinitely)
 86            # Self loops in the multidirected graph complete themselves
 87            # In practice, this means often the same model can be used to compute prompt input and response output
 88            # Unfortunately, this doesn't always work in all modalities, ex. Image to Image.
 89            # This condition is meant to solve the case of non-text self-loop edge being an incomplete transformation
 90
 91            if mode_in == mode_out and mode_in != "text":  # Its not a great solution, but it works for the moment
 92                orig_mode_out = mode_out
 93                mode_out = "text"
 94                self.coord_path = nx.bidirectional_shortest_path(self.intent_graph, mode_in, mode_out)
 95                self.coord_path.append(orig_mode_out)
 96            else:
 97                self.coord_path = nx.bidirectional_shortest_path(self.intent_graph, mode_in, mode_out)
 98                if len(self.coord_path) == 1:
 99                    self.coord_path.append(mode_out)  # this behaviour likely to change in future
100
101        else:
102            nfo("No Path available...\n")
103
104    def set_registry_entries(self) -> None:
105        """Populate models list for text fields
106        Check if model has been adjusted, if so adjust list
107        1.0 weight bottom, <1.0 weight top"""
108
109        try:
110            self.registry_entries = self.pull_path_entries(self.intent_graph, self.coord_path)
111        except KeyError as error_log:
112            dbug(error_log)
113            return ["", ""]
114        idx = 0
115        self.models = []
116
117        if self.registry_entries:
118            for edge, registry in enumerate(self.registry_entries):
119                model = registry["entry"].model
120                dbuq(f"node {edge}")
121                adj_model = (os.path.basename(model), edge)
122                self.models.append(adj_model)
123            self.weight_idx = self.weight_idx or []
124            for model in self.weight_idx:
125                if model in self.models:
126                    self.models.remove(model)
127                    adj_model = (f"*{model[0]}", model[1])
128                    self.models.insert(idx, adj_model)
129                    idx += 1
130
131    def edit_weight(self, edge_number: str, mode_in: str, mode_out: str) -> None:
132        """Determine entry edge, determine index, then adjust weight\n
133        :param edge_number: Text pattern from `models` class attribute to identify the model by
134        :param mode_in: The conversion type, representing a source graph node
135        :param mode_out: The target type, , representing a source graph node
136        :raises ValueError: No models fit the request
137        """
138
139        self.weight_idx = self.weight_idx or []
140
141        try:
142            if not nx.has_path(self.intent_graph, mode_in, mode_out):
143                raise KeyError()
144            model = self.intent_graph[mode_in][mode_out][edge_number]["entry"].model
145        except KeyError as error_log:
146            nfo(
147                f"Failed to adjust weight of '{edge_number}' within registry contents \
148                '{self.intent_graph} {mode_in} {mode_out}'. Model or registry entry not found. "
149            )
150            dbug(error_log)
151            return self.set_registry_entries()
152
153        weight = self.intent_graph[mode_in][mode_out][edge_number]["weight"]
154        item = (os.path.basename(model), edge_number)
155        nfo(f" model : {model}  weight: {weight} ")
156
157        if weight < 1.0:
158            self.intent_graph[mode_in][mode_out][edge_number]["weight"] = round(weight + 0.1, 1)
159            self.models = [((f"*{os.path.basename(model)}", edge_number))]
160            if item in self.weight_idx:
161                self.weight_idx.remove(item)
162        else:
163            self.intent_graph[mode_in][mode_out][edge_number]["weight"] = round(weight - 0.1, 1)
164            self.weight_idx.append(item)
165        self.set_registry_entries()
166
167    def pull_path_entries(self, nx_graph: nx.Graph, traced_path: list[tuple]) -> None:
168        """Create operating instructions from user input
169        Trace the next hop along the path, collect all compatible models
170        Set current model based on weight and next available"""
171
172        registry_entries = []
173        if traced_path is not None and nx.has_path(nx_graph, traced_path[0], traced_path[1]):
174            registry_entries = [  # ruff : noqa
175                nx_graph[traced_path[index]][traced_path[index + 1]][hop]  #
176                for index in range(len(traced_path) - 1)  #
177                for hop in nx_graph[traced_path[index]][traced_path[index + 1]]  #
178            ]
179        return registry_entries
IntentProcessor( intent_graph: networkx.classes.multidigraph.MultiDiGraph = <networkx.classes.multidigraph.MultiDiGraph object>)
25    def __init__(self, intent_graph: nx.MultiDiGraph = nx.MultiDiGraph()) -> None:
26        """
27        Create instance of graph processor & initialize objectieves for tracing paths\n
28        :param nx_graph:Preassembled graph of models to substitute, default uses nx.MultiDiGraph()
29
30        ========================================================\n
31        ### GIVEN\n
32        A : The list of `VALID CONVERSIONS` contains all of Zodiac's supported generative modalities\n
33        B : The graph is populated directly from the contents of the list in A\n
34        Thus: All possible node start and end points listed in A are included in graph B.\n
35        Therefore : It is impossible to call a node that does not exist.\n
36        """
37        from zodiac.providers.constants import VALID_CONVERSIONS
38
39        self.intent_graph = intent_graph
40        self.intent_graph.add_nodes_from(VALID_CONVERSIONS)

Create instance of graph processor & initialize objectieves for tracing paths

Parameters
  • nx_graph: Preassembled graph of models to substitute, default uses nx.MultiDiGraph()

========================================================

GIVEN

A : The list of VALID CONVERSIONS contains all of Zodiac's supported generative modalities

B : The graph is populated directly from the contents of the list in A

Thus: All possible node start and end points listed in A are included in graph B.

Therefore : It is impossible to call a node that does not exist.

intent_graph: Optional[dict[networkx.classes.graph.Graph]] = None
coord_path: Optional[list[str]] = None
registry_entries: Optional[list[dict[dict]]] = None
models: Optional[list[tuple[str]]] = None
weight_idx: Optional[list[str]] = None
async def calc_graph(self, registry_entries: Optional[list] = None) -> None:
42    async def calc_graph(self, registry_entries: Optional[list] = None) -> None:
43        """Generate graph of coordinate pairs from valid conversions\n
44        Model libraries are auto-detected from cache loading\n
45        :param registry_data: Registry function or method of calling registry, defaults to
46        :return: Graph modeling all current ML/AI tasks appended with model data
47
48        ========================================================\n
49        ### GIVEN\n
50        A : The set of all models M on the executing system\n
51        B : P is the randomly distributed set of start and end points required to graph M\n
52        Thus: Because of the randomness of B, the set P is unlikely to construct a complete graph attached all available points.\n
53        Therefore : While we can trust a node exists, we **CANNOT** trust the system has an edge to reach it\n
54        """
55        # import asyncio
56
57        if not registry_entries:
58            registry_entries = await register_models()
59        nfo("Building graph...")
60
61        if registry_entries is None:
62            nfo("Registry error, graph attributes not applied.")
63        elif len(self.intent_graph.edges) > 0:
64            nfo("Edges already calculated")
65            return self.intent_graph
66        else:
67            for model in registry_entries:
68                try:
69                    self.intent_graph.add_edges_from(model.available_tasks, entry=model, weight=1.0)
70                except AttributeError as error_log:
71                    dbug(error_log)
72                    nfo("Error: Registry initialized but not populated with data. Graph could not create edges.")
73
74        nfo("Complete {self.intent_graph}")
75        return self.intent_graph

Generate graph of coordinate pairs from valid conversions

Model libraries are auto-detected from cache loading

Parameters
  • registry_data: Registry function or method of calling registry, defaults to
Returns

Graph modeling all current ML/AI tasks appended with model data

========================================================

GIVEN

A : The set of all models M on the executing system

B : P is the randomly distributed set of start and end points required to graph M

Thus: Because of the randomness of B, the set P is unlikely to construct a complete graph attached all available points.

Therefore : While we can trust a node exists, we CANNOT trust the system has an edge to reach it

def set_path(self, mode_in: str, mode_out: str) -> None:
 77    def set_path(self, mode_in: str, mode_out: str) -> None:
 78        """Find a valid path from current state (mode_in) to designated state (mode_out)\n
 79        :param mode_in: Input prompt type or starting state/states
 80        :type mode_in: str
 81        :param mode_out: The user-selected ending-state
 82        :type mode_out: str
 83        """
 84
 85        if nx.has_path(self.intent_graph, mode_in, mode_out):  # Ensure path exists (otherwise 'bidirectional' may loop infinitely)
 86            # Self loops in the multidirected graph complete themselves
 87            # In practice, this means often the same model can be used to compute prompt input and response output
 88            # Unfortunately, this doesn't always work in all modalities, ex. Image to Image.
 89            # This condition is meant to solve the case of non-text self-loop edge being an incomplete transformation
 90
 91            if mode_in == mode_out and mode_in != "text":  # Its not a great solution, but it works for the moment
 92                orig_mode_out = mode_out
 93                mode_out = "text"
 94                self.coord_path = nx.bidirectional_shortest_path(self.intent_graph, mode_in, mode_out)
 95                self.coord_path.append(orig_mode_out)
 96            else:
 97                self.coord_path = nx.bidirectional_shortest_path(self.intent_graph, mode_in, mode_out)
 98                if len(self.coord_path) == 1:
 99                    self.coord_path.append(mode_out)  # this behaviour likely to change in future
100
101        else:
102            nfo("No Path available...\n")

Find a valid path from current state (mode_in) to designated state (mode_out)

Parameters
  • mode_in: Input prompt type or starting state/states
  • mode_out: The user-selected ending-state
def set_registry_entries(self) -> None:
104    def set_registry_entries(self) -> None:
105        """Populate models list for text fields
106        Check if model has been adjusted, if so adjust list
107        1.0 weight bottom, <1.0 weight top"""
108
109        try:
110            self.registry_entries = self.pull_path_entries(self.intent_graph, self.coord_path)
111        except KeyError as error_log:
112            dbug(error_log)
113            return ["", ""]
114        idx = 0
115        self.models = []
116
117        if self.registry_entries:
118            for edge, registry in enumerate(self.registry_entries):
119                model = registry["entry"].model
120                dbuq(f"node {edge}")
121                adj_model = (os.path.basename(model), edge)
122                self.models.append(adj_model)
123            self.weight_idx = self.weight_idx or []
124            for model in self.weight_idx:
125                if model in self.models:
126                    self.models.remove(model)
127                    adj_model = (f"*{model[0]}", model[1])
128                    self.models.insert(idx, adj_model)
129                    idx += 1

Populate models list for text fields Check if model has been adjusted, if so adjust list 1.0 weight bottom, <1.0 weight top

def edit_weight(self, edge_number: str, mode_in: str, mode_out: str) -> None:
131    def edit_weight(self, edge_number: str, mode_in: str, mode_out: str) -> None:
132        """Determine entry edge, determine index, then adjust weight\n
133        :param edge_number: Text pattern from `models` class attribute to identify the model by
134        :param mode_in: The conversion type, representing a source graph node
135        :param mode_out: The target type, , representing a source graph node
136        :raises ValueError: No models fit the request
137        """
138
139        self.weight_idx = self.weight_idx or []
140
141        try:
142            if not nx.has_path(self.intent_graph, mode_in, mode_out):
143                raise KeyError()
144            model = self.intent_graph[mode_in][mode_out][edge_number]["entry"].model
145        except KeyError as error_log:
146            nfo(
147                f"Failed to adjust weight of '{edge_number}' within registry contents \
148                '{self.intent_graph} {mode_in} {mode_out}'. Model or registry entry not found. "
149            )
150            dbug(error_log)
151            return self.set_registry_entries()
152
153        weight = self.intent_graph[mode_in][mode_out][edge_number]["weight"]
154        item = (os.path.basename(model), edge_number)
155        nfo(f" model : {model}  weight: {weight} ")
156
157        if weight < 1.0:
158            self.intent_graph[mode_in][mode_out][edge_number]["weight"] = round(weight + 0.1, 1)
159            self.models = [((f"*{os.path.basename(model)}", edge_number))]
160            if item in self.weight_idx:
161                self.weight_idx.remove(item)
162        else:
163            self.intent_graph[mode_in][mode_out][edge_number]["weight"] = round(weight - 0.1, 1)
164            self.weight_idx.append(item)
165        self.set_registry_entries()

Determine entry edge, determine index, then adjust weight

Parameters
  • edge_number: Text pattern from models class attribute to identify the model by
  • mode_in: The conversion type, representing a source graph node
  • mode_out: The target type, , representing a source graph node
Raises
  • ValueError: No models fit the request
def pull_path_entries( self, nx_graph: networkx.classes.graph.Graph, traced_path: list[tuple]) -> None:
167    def pull_path_entries(self, nx_graph: nx.Graph, traced_path: list[tuple]) -> None:
168        """Create operating instructions from user input
169        Trace the next hop along the path, collect all compatible models
170        Set current model based on weight and next available"""
171
172        registry_entries = []
173        if traced_path is not None and nx.has_path(nx_graph, traced_path[0], traced_path[1]):
174            registry_entries = [  # ruff : noqa
175                nx_graph[traced_path[index]][traced_path[index + 1]][hop]  #
176                for index in range(len(traced_path) - 1)  #
177                for hop in nx_graph[traced_path[index]][traced_path[index + 1]]  #
178            ]
179        return registry_entries

Create operating instructions from user input Trace the next hop along the path, collect all compatible models Set current model based on weight and next available