versionhq 1.2.1.5__py3-none-any.whl → 1.2.1.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- versionhq/__init__.py +4 -2
- versionhq/task/formation.py +1 -1
- versionhq/task/model.py +6 -6
- versionhq/task_graph/colors.py +28 -0
- versionhq/task_graph/draft.py +101 -0
- versionhq/task_graph/model.py +155 -141
- {versionhq-1.2.1.5.dist-info → versionhq-1.2.1.7.dist-info}/METADATA +10 -18
- {versionhq-1.2.1.5.dist-info → versionhq-1.2.1.7.dist-info}/RECORD +11 -9
- {versionhq-1.2.1.5.dist-info → versionhq-1.2.1.7.dist-info}/LICENSE +0 -0
- {versionhq-1.2.1.5.dist-info → versionhq-1.2.1.7.dist-info}/WHEEL +0 -0
- {versionhq-1.2.1.5.dist-info → versionhq-1.2.1.7.dist-info}/top_level.txt +0 -0
versionhq/__init__.py
CHANGED
@@ -28,9 +28,10 @@ from versionhq.memory.contextual_memory import ContextualMemory
|
|
28
28
|
from versionhq.memory.model import ShortTermMemory,LongTermMemory, UserMemory, MemoryItem
|
29
29
|
|
30
30
|
from versionhq.task.formation import form_agent_network
|
31
|
+
from versionhq.task_graph.draft import workflow
|
31
32
|
|
32
33
|
|
33
|
-
__version__ = "1.2.1.
|
34
|
+
__version__ = "1.2.1.7"
|
34
35
|
__all__ = [
|
35
36
|
"Agent",
|
36
37
|
|
@@ -88,5 +89,6 @@ __all__ = [
|
|
88
89
|
"UserMemory",
|
89
90
|
"MemoryItem",
|
90
91
|
|
91
|
-
"form_agent_network"
|
92
|
+
"form_agent_network",
|
93
|
+
"workflow",
|
92
94
|
]
|
versionhq/task/formation.py
CHANGED
@@ -39,7 +39,7 @@ def form_agent_network(
|
|
39
39
|
pass
|
40
40
|
|
41
41
|
case str():
|
42
|
-
matched = [item for item in Formation.
|
42
|
+
matched = [item for item in Formation.s_ if item == formation.upper()]
|
43
43
|
if matched:
|
44
44
|
formation = getattr(Formation, matched[0])
|
45
45
|
else:
|
versionhq/task/model.py
CHANGED
@@ -4,6 +4,7 @@ import datetime
|
|
4
4
|
import uuid
|
5
5
|
import inspect
|
6
6
|
import enum
|
7
|
+
from textwrap import dedent
|
7
8
|
from concurrent.futures import Future
|
8
9
|
from hashlib import md5
|
9
10
|
from typing import Any, Dict, List, Set, Optional, Callable, Type
|
@@ -288,7 +289,7 @@ class Task(BaseModel):
|
|
288
289
|
should_evaluate: bool = Field(default=False, description="True to run the evaluation flow")
|
289
290
|
eval_criteria: Optional[List[str]] = Field(default_factory=list, description="criteria to evaluate the outcome. i.e., fit to the brand tone")
|
290
291
|
|
291
|
-
# recording
|
292
|
+
# recording !# REFINEME - eval_callbacks
|
292
293
|
processed_agents: Set[str] = Field(default_factory=set, description="store roles of the agents that executed the task")
|
293
294
|
tool_errors: int = 0
|
294
295
|
delegations: int = 0
|
@@ -364,7 +365,7 @@ Ref. Output image: {output_formats_to_follow}
|
|
364
365
|
else:
|
365
366
|
output_prompt = "Return your response as a valid JSON serializable string, enclosed in double quotes. Do not use single quotes, trailing commas, or other non-standard JSON syntax."
|
366
367
|
|
367
|
-
return output_prompt
|
368
|
+
return dedent(output_prompt)
|
368
369
|
|
369
370
|
|
370
371
|
def _draft_context_prompt(self, context: Any) -> str:
|
@@ -374,7 +375,7 @@ Ref. Output image: {output_formats_to_follow}
|
|
374
375
|
|
375
376
|
context_to_add = None
|
376
377
|
if not context:
|
377
|
-
Logger().log(level="error", color="red", message="Missing a context to add to the prompt. We'll return ''.")
|
378
|
+
# Logger().log(level="error", color="red", message="Missing a context to add to the prompt. We'll return ''.")
|
378
379
|
return context_to_add
|
379
380
|
|
380
381
|
match context:
|
@@ -403,7 +404,7 @@ Ref. Output image: {output_formats_to_follow}
|
|
403
404
|
case _:
|
404
405
|
pass
|
405
406
|
|
406
|
-
return context_to_add
|
407
|
+
return dedent(context_to_add)
|
407
408
|
|
408
409
|
|
409
410
|
def _prompt(self, model_provider: str = None, context: Optional[Any] = None) -> str:
|
@@ -477,7 +478,7 @@ Ref. Output image: {output_formats_to_follow}
|
|
477
478
|
return output
|
478
479
|
|
479
480
|
else:
|
480
|
-
r = str(raw).replace("{'", '{"').replace("{ '", '{"').replace("': '", '": "').replace("'}", '"}').replace("' }", '"}').replace("', '", '", "').replace("['", '["').replace("[ '", '[ "').replace("']", '"]').replace("' ]", '" ]').replace("{\n'", '{"').replace("{\'", '{"').replace("true", "True").replace("false", "False")
|
481
|
+
r = str(raw).strip().replace("{'", '{"').replace("{ '", '{"').replace("': '", '": "').replace("'}", '"}').replace("' }", '"}').replace("', '", '", "').replace("['", '["').replace("[ '", '[ "').replace("']", '"]').replace("' ]", '" ]').replace("{\n'", '{"').replace("{\'", '{"').replace("true", "True").replace("false", "False").replace('\"', "'")
|
481
482
|
j = json.dumps(eval(r))
|
482
483
|
output = json.loads(j)
|
483
484
|
|
@@ -593,7 +594,6 @@ Ref. Output image: {output_formats_to_follow}
|
|
593
594
|
return self._execute_async(agent=agent, context=context)
|
594
595
|
|
595
596
|
|
596
|
-
|
597
597
|
def _execute_sync(self, agent, context: Optional[Any] = None) -> TaskOutput:
|
598
598
|
"""Executes the task synchronously."""
|
599
599
|
return self._execute_core(agent, context)
|
@@ -0,0 +1,28 @@
|
|
1
|
+
white = "#fafafa"
|
2
|
+
black = "#191b1b"
|
3
|
+
darkgrey = "#363636"
|
4
|
+
grey = "#414141"
|
5
|
+
primary = "#B9005B"
|
6
|
+
orange = "#ffb713"
|
7
|
+
lightgreen = "#b8fff5"
|
8
|
+
green = "#00d1b2"
|
9
|
+
darkgreen = "#169d87"
|
10
|
+
darkergreen = "#026b5d"
|
11
|
+
|
12
|
+
|
13
|
+
# $black: #191b1b;
|
14
|
+
# $true-white: #ffffff;
|
15
|
+
# $white: #fafafa;
|
16
|
+
# $grey: #414141;
|
17
|
+
# $light-grey: #f9f9f9;
|
18
|
+
# $border-color: #dedede;
|
19
|
+
# $primary: #B9005B;
|
20
|
+
# $primary-hovered: #9D034F;
|
21
|
+
# $gradient-main: linear-gradient(to right $black $primary);
|
22
|
+
# $linkedin-blue: #0077B5;
|
23
|
+
# $bulma-primary: #00d1b2;
|
24
|
+
# $bluma-primary-dark: #169d87;
|
25
|
+
# $bulma-grey-light: hsl(0 0%, 71%);
|
26
|
+
# $bulma-grey: hsl(0, 0%, 48%);
|
27
|
+
# $bulma-dark: hsl(0, 0%, 21%); //#363636
|
28
|
+
# $bulma-white-ter: #F5F5F5;
|
@@ -0,0 +1,101 @@
|
|
1
|
+
import sys
|
2
|
+
from typing import Type, Any
|
3
|
+
from pydantic import BaseModel
|
4
|
+
from pydantic._internal._model_construction import ModelMetaclass
|
5
|
+
from textwrap import dedent
|
6
|
+
if 'pydantic.main' not in sys.modules:
|
7
|
+
import pydantic.main
|
8
|
+
|
9
|
+
sys.modules['pydantic.main'].ModelMetaclass = ModelMetaclass
|
10
|
+
|
11
|
+
from versionhq.agent.model import Agent
|
12
|
+
from versionhq.task.model import ResponseField
|
13
|
+
from versionhq.task_graph.model import TaskGraph, Task, DependencyType, Node, TaskStatus
|
14
|
+
from versionhq._utils.logger import Logger
|
15
|
+
|
16
|
+
|
17
|
+
def workflow(final_output: Type[BaseModel], context: Any = None, human: bool = True) -> TaskGraph | None:
|
18
|
+
"""
|
19
|
+
Generate a TaskGraph object to generate the givne final_output most resource-efficiently.
|
20
|
+
"""
|
21
|
+
|
22
|
+
if not final_output or not isinstance(final_output, ModelMetaclass):
|
23
|
+
Logger().log(level="error", message="Missing an expected output in Pydantic model.", color="red")
|
24
|
+
return None
|
25
|
+
|
26
|
+
final_output_prompt = ", ".join([k for k in final_output.model_fields.keys()])
|
27
|
+
|
28
|
+
if not final_output_prompt:
|
29
|
+
Logger().log(level="error", message="Expected output is in invalid format.", color="red")
|
30
|
+
return None
|
31
|
+
|
32
|
+
context_prompt = f'We are designing a resource-efficient workflow using graph algorithm concepts to achieve the following goal: {final_output_prompt}.'
|
33
|
+
|
34
|
+
dep_type_prompt = ", ".join([k for k in DependencyType._member_map_.keys()])
|
35
|
+
|
36
|
+
graph_expert = Agent(
|
37
|
+
role="vhq-G Expert",
|
38
|
+
goal="design the most resource-efficient workflow graph to achieve the given goal",
|
39
|
+
knowledge_sources=[
|
40
|
+
"https://en.wikipedia.org/wiki/Graph_theory",
|
41
|
+
# "https://www.geeksforgeeks.org/graph-data-structure-and-algorithms/?ref=lbp",
|
42
|
+
"https://www.geeksforgeeks.org/graph-and-its-representations/",
|
43
|
+
", ".join([k for k in DependencyType._member_map_.keys()]),
|
44
|
+
],
|
45
|
+
llm="gemini-2.0",
|
46
|
+
use_memory=True,
|
47
|
+
maxit=1,
|
48
|
+
max_retry_limit=1,
|
49
|
+
)
|
50
|
+
|
51
|
+
task = Task(
|
52
|
+
description=f"Design a resource-efficient workflow to achieve the following goal: {final_output_prompt}. The workflow should consist of a list of tasks, each with the following information:\nname: A concise name of the task\ndescription: A concise description of the task.\nconnections: A list of target tasks that this task connects to.\ndependency_types: The type of dependency between this task and each of its connected task. Use the following dependency types: {dep_type_prompt}.\n\nPrioritize minimizing resource consumption (computation, memory, and data transfer) when defining tasks, connections, and dependencies. Consider how data is passed between tasks and aim to reduce unnecessary data duplication or transfer. Explain any design choices made to optimize resource usage.",
|
53
|
+
response_fields=[
|
54
|
+
ResponseField(title="tasks", data_type=list, items=dict, properties=[
|
55
|
+
ResponseField(title="name", data_type=str),
|
56
|
+
ResponseField(title="description", data_type=str),
|
57
|
+
ResponseField(title="connections", data_type=list, items=str),
|
58
|
+
ResponseField(title="dependency_types", data_type=list, items=str),
|
59
|
+
]
|
60
|
+
)
|
61
|
+
]
|
62
|
+
)
|
63
|
+
res = task.execute(agent=graph_expert, context=[context_prompt, context])
|
64
|
+
|
65
|
+
if not res:
|
66
|
+
return None
|
67
|
+
|
68
|
+
task_items = res.json_dict["tasks"]
|
69
|
+
tasks = [Task(name=item["name"], description=item["description"]) for item in task_items]
|
70
|
+
nodes = [Node(task=task) for task in tasks]
|
71
|
+
task_graph = TaskGraph(
|
72
|
+
nodes={node.identifier: node for node in nodes},
|
73
|
+
concl=final_output,
|
74
|
+
should_reform=True,
|
75
|
+
)
|
76
|
+
|
77
|
+
for res in task_items:
|
78
|
+
if res["connections"]:
|
79
|
+
dependency_types = [DependencyType[dt] if DependencyType[dt] else DependencyType.FINISH_TO_START for dt in res["dependency_types"]]
|
80
|
+
|
81
|
+
for i, target_task_name in enumerate(res["connections"]):
|
82
|
+
source = [v for k, v in task_graph.nodes.items() if v.task.name == res["name"]][0]
|
83
|
+
target = [v for k, v in task_graph.nodes.items() if v.task.name == target_task_name][0]
|
84
|
+
dependency_type = dependency_types[i]
|
85
|
+
task_graph.add_dependency(source_node_identifier=source.identifier, target_node_identifier=target.identifier, dependency_type=dependency_type)
|
86
|
+
|
87
|
+
## test purpose
|
88
|
+
# task_graph.visualize()
|
89
|
+
|
90
|
+
# if human:
|
91
|
+
# print('Proceed? Y/n:')
|
92
|
+
# x = input()
|
93
|
+
|
94
|
+
# if x.lower() == "y":
|
95
|
+
# print("ok. generating agent network")
|
96
|
+
|
97
|
+
# else:
|
98
|
+
# request = input("request?")
|
99
|
+
# print('ok. regenerating the graph based on your input: ', request)
|
100
|
+
|
101
|
+
return task_graph
|
versionhq/task_graph/model.py
CHANGED
@@ -4,8 +4,9 @@ import networkx as nx
|
|
4
4
|
import matplotlib.pyplot as plt
|
5
5
|
from abc import ABC
|
6
6
|
from typing import List, Any, Optional, Callable, Dict, Type, Tuple
|
7
|
+
from typing_extensions import Self
|
7
8
|
|
8
|
-
from pydantic import BaseModel, InstanceOf, Field, UUID4, field_validator
|
9
|
+
from pydantic import BaseModel, InstanceOf, Field, UUID4, field_validator, model_validator
|
9
10
|
from pydantic_core import PydanticCustomError
|
10
11
|
|
11
12
|
from versionhq.task.model import Task, TaskOutput
|
@@ -36,32 +37,13 @@ class DependencyType(enum.Enum):
|
|
36
37
|
START_TO_FINISH = "SF" # Task B finishes when Task A starts
|
37
38
|
|
38
39
|
|
39
|
-
|
40
|
-
# class TriggerEvent(enum.Enum):
|
41
|
-
# """
|
42
|
-
# Concise enumeration of key trigger events for task execution.
|
43
|
-
# """
|
44
|
-
# IMMEDIATE = 0 # execute immediately
|
45
|
-
# DEPENDENCIES_MET = 1 # All/required dependencies are satisfied
|
46
|
-
# RESOURCES_AVAILABLE = 2 # Necessary resources are available
|
47
|
-
# SCHEDULED_TIME = 3 # Scheduled start time or time window reached
|
48
|
-
# EXTERNAL_EVENT = 4 # Triggered by an external event/message
|
49
|
-
# DATA_AVAILABLE = 5 # Required data is available both internal/external
|
50
|
-
# APPROVAL_RECEIVED = 6 # Necessary approvals have been granted
|
51
|
-
# STATUS_CHANGED = 7 # Relevant task/system status has changed
|
52
|
-
# RULE_MET = 8 # A predefined rule or condition has been met
|
53
|
-
# MANUAL_TRIGGER = 9 # Manually initiated by a user
|
54
|
-
# ERROR_HANDLED = 10 # A previous error/exception has been handled
|
55
|
-
|
56
|
-
|
57
|
-
|
58
40
|
class Node(BaseModel):
|
59
41
|
"""
|
60
42
|
A class to store a node object.
|
61
43
|
"""
|
44
|
+
|
62
45
|
id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
|
63
46
|
task: InstanceOf[Task] = Field(default=None)
|
64
|
-
# trigger_event: TriggerEvent = Field(default=TriggerEvent.IMMEDIATE, description="store trigger event for starting the task execution")
|
65
47
|
in_degree_nodes: List[Any] = Field(default_factory=list, description="list of Node objects")
|
66
48
|
out_degree_nodes: List[Any] = Field(default_factory=list, description="list of Node objects")
|
67
49
|
assigned_to: InstanceOf[Agent] = Field(default=None)
|
@@ -70,14 +52,14 @@ class Node(BaseModel):
|
|
70
52
|
|
71
53
|
@field_validator("id", mode="before")
|
72
54
|
@classmethod
|
73
|
-
def
|
55
|
+
def _deny_user_set_id(cls, v: Optional[UUID4]) -> None:
|
74
56
|
if v:
|
75
|
-
raise PydanticCustomError("may_not_set_field", "This field is not to be set by
|
57
|
+
raise PydanticCustomError("may_not_set_field", "This field is not to be set by client.", {})
|
58
|
+
|
76
59
|
|
77
60
|
def is_independent(self) -> bool:
|
78
61
|
return not self.in_degree_nodes and not self.out_degree_nodes
|
79
62
|
|
80
|
-
|
81
63
|
def handle_task_execution(self, agent: Agent = None, context: str = None) -> TaskOutput | None:
|
82
64
|
"""
|
83
65
|
Start task execution and update status accordingly.
|
@@ -109,11 +91,14 @@ class Node(BaseModel):
|
|
109
91
|
|
110
92
|
@property
|
111
93
|
def identifier(self) -> str:
|
112
|
-
"""Unique identifier
|
94
|
+
"""Unique identifier of the node"""
|
113
95
|
return f"{str(self.id)}"
|
114
96
|
|
115
97
|
def __str__(self):
|
116
|
-
|
98
|
+
if self.task:
|
99
|
+
return f"{self.identifier}: {self.task.name if self.task.name else self.task.description[0: 12]}"
|
100
|
+
else:
|
101
|
+
return self.identifier
|
117
102
|
|
118
103
|
|
119
104
|
class Edge(BaseModel):
|
@@ -136,6 +121,17 @@ class Edge(BaseModel):
|
|
136
121
|
data_transfer: bool = Field(default=True, description="whether the data transfer is required. by default transfer plane text output from in-degree nodes as context")
|
137
122
|
|
138
123
|
|
124
|
+
def _response_schema(self) -> Type[BaseModel]:
|
125
|
+
class EdgeResponseSchema(BaseModel):
|
126
|
+
wight: float
|
127
|
+
dependecy_type: str
|
128
|
+
required: bool
|
129
|
+
need_condition: bool
|
130
|
+
lag_in_sec: float
|
131
|
+
|
132
|
+
return EdgeResponseSchema
|
133
|
+
|
134
|
+
|
139
135
|
def dependency_met(self) -> bool:
|
140
136
|
"""
|
141
137
|
Defines if the dependency is ready to execute:
|
@@ -153,28 +149,28 @@ class Edge(BaseModel):
|
|
153
149
|
match self.dependency_type:
|
154
150
|
case DependencyType.FINISH_TO_START:
|
155
151
|
"""target starts after source finishes"""
|
156
|
-
if self.source.status == TaskStatus.COMPLETED:
|
152
|
+
if not self.source or self.source.status == TaskStatus.COMPLETED:
|
157
153
|
return self.condition(**self.conditon_kwargs) if self.condition else True
|
158
154
|
else:
|
159
155
|
return False
|
160
156
|
|
161
157
|
case DependencyType.START_TO_START:
|
162
158
|
"""target starts when source starts"""
|
163
|
-
if self.source.status != TaskStatus.NOT_STARTED:
|
159
|
+
if not self.source or self.source.status != TaskStatus.NOT_STARTED:
|
164
160
|
return self.condition(**self.conditon_kwargs) if self.condition else True
|
165
161
|
else:
|
166
162
|
return False
|
167
163
|
|
168
164
|
case DependencyType.FINISH_TO_FINISH:
|
169
165
|
"""target finish when source start"""
|
170
|
-
if self.source.status != TaskStatus.COMPLETED:
|
166
|
+
if not self.source or self.source.status != TaskStatus.COMPLETED:
|
171
167
|
return self.condition(**self.conditon_kwargs) if self.condition else True
|
172
168
|
else:
|
173
169
|
return False
|
174
170
|
|
175
171
|
case DependencyType.START_TO_FINISH:
|
176
172
|
"""target finishes when source start"""
|
177
|
-
if self.source.status == TaskStatus.IN_PROGRESS:
|
173
|
+
if not self.source or self.source.status == TaskStatus.IN_PROGRESS:
|
178
174
|
return self.condition(**self.conditon_kwargs) if self.condition else True
|
179
175
|
else:
|
180
176
|
return False
|
@@ -190,10 +186,9 @@ class Edge(BaseModel):
|
|
190
186
|
return None
|
191
187
|
|
192
188
|
if not self.dependency_met():
|
193
|
-
Logger(verbose=True).log(level="warning", message="Dependencies not met. We'll
|
189
|
+
Logger(verbose=True).log(level="warning", message="Dependencies not met. We'll return None.", color="yellow")
|
194
190
|
return None
|
195
191
|
|
196
|
-
|
197
192
|
if self.lag:
|
198
193
|
import time
|
199
194
|
time.sleep(self.lag)
|
@@ -207,29 +202,42 @@ class Graph(ABC, BaseModel):
|
|
207
202
|
"""
|
208
203
|
An abstract class to store G using NetworkX library.
|
209
204
|
"""
|
210
|
-
|
211
205
|
directed: bool = Field(default=False, description="Whether the graph is directed")
|
212
206
|
graph: Type[nx.Graph] = Field(default=None)
|
213
|
-
nodes: Dict[str,
|
214
|
-
edges: Dict[str,
|
207
|
+
nodes: Dict[str, Node] = Field(default_factory=dict, description="identifier: Node - for the sake of ")
|
208
|
+
edges: Dict[str, Edge] = Field(default_factory=dict)
|
215
209
|
|
216
210
|
def __init__(self, directed: bool = False, **kwargs):
|
217
211
|
super().__init__(directed=directed, **kwargs)
|
218
|
-
self.graph = nx.DiGraph() if self.directed else nx.Graph()
|
212
|
+
self.graph = nx.DiGraph(directed=True) if self.directed else nx.Graph()
|
213
|
+
|
219
214
|
|
220
215
|
def _return_node_object(self, node_identifier) -> Node | None:
|
221
|
-
|
216
|
+
match = [v for k, v in self.nodes.items() if k == node_identifier]
|
217
|
+
|
218
|
+
if match:
|
219
|
+
node = match[0] if isinstance(match[0], Node) else match[0]["node"] if "node" in match[0] else None
|
220
|
+
return node
|
221
|
+
else:
|
222
|
+
return None
|
222
223
|
|
223
224
|
def add_node(self, node: Node) -> None:
|
224
|
-
|
225
|
+
if node.identifier in self.nodes.keys():
|
226
|
+
return
|
227
|
+
self.graph.add_node(node.identifier, node=node)
|
225
228
|
self.nodes[node.identifier] = node
|
226
229
|
|
227
230
|
def add_edge(self, source: str, target: str, edge: Edge) -> None:
|
228
|
-
self.graph.add_edge(source, target,
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
edge.
|
231
|
+
self.graph.add_edge(source, target, edge=edge)
|
232
|
+
|
233
|
+
source_node, target_node = self._return_node_object(source), self._return_node_object(target)
|
234
|
+
|
235
|
+
edge.source = source_node
|
236
|
+
source_node.out_degree_nodes.append(target_node)
|
237
|
+
|
238
|
+
edge.target = target_node
|
239
|
+
target_node.in_degree_nodes.append(source_node)
|
240
|
+
|
233
241
|
self.edges[(source, target)] = edge
|
234
242
|
|
235
243
|
def add_weighted_edges_from(self, edges):
|
@@ -244,11 +252,11 @@ class Graph(ABC, BaseModel):
|
|
244
252
|
def get_out_degree(self, node: Node) -> int:
|
245
253
|
return self.graph.out_degree(node)
|
246
254
|
|
247
|
-
def find_start_nodes(self) ->
|
248
|
-
return [v for
|
255
|
+
def find_start_nodes(self) -> List[Node]:
|
256
|
+
return [v for v in self.nodes.values() if v.in_degrees == 0 and v.out_degrees > 0]
|
249
257
|
|
250
|
-
def find_end_nodes(self) ->
|
251
|
-
return [v for
|
258
|
+
def find_end_nodes(self) -> List[Node]:
|
259
|
+
return [v for v in self.nodes.values() if v.out_degrees == 0 and v.in_degrees > 0]
|
252
260
|
|
253
261
|
def find_critical_end_node(self) -> Node | None:
|
254
262
|
"""
|
@@ -310,51 +318,26 @@ class Graph(ABC, BaseModel):
|
|
310
318
|
return False
|
311
319
|
|
312
320
|
|
313
|
-
def visualize(self, title: str = "Task Graph", pos: Any = None, **graph_config):
|
314
|
-
pos = pos if pos else nx.spring_layout(self.graph, seed=42)
|
315
|
-
nx.draw(
|
316
|
-
self.graph,
|
317
|
-
pos,
|
318
|
-
with_labels=True, node_size=700, node_color="skyblue", font_size=10, font_color="black", arrowstyle='-|>', arrowsize=20, arrows=True,
|
319
|
-
**graph_config
|
320
|
-
)
|
321
|
-
edge_labels = {}
|
322
|
-
for u, v, data in self.graph.edges(data=True):
|
323
|
-
edge = self.edges.get((u,v))
|
324
|
-
if edge:
|
325
|
-
label_parts = []
|
326
|
-
if edge.type:
|
327
|
-
label_parts.append(f"Type: {edge.type}")
|
328
|
-
if edge.duration is not None:
|
329
|
-
label_parts.append(f"Duration: {edge.duration}")
|
330
|
-
if edge.lag is not None:
|
331
|
-
label_parts.append(f"Lag: {edge.lag}")
|
332
|
-
edge_labels[(u, v)] = "\n".join(label_parts) # Combine labels with newlines
|
333
|
-
nx.draw_networkx_edge_labels(self.graph, pos, edge_labels=edge_labels)
|
334
|
-
plt.title(title)
|
335
|
-
plt.show()
|
336
|
-
|
337
|
-
|
338
321
|
class TaskGraph(Graph):
|
339
322
|
id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
|
340
323
|
should_reform: bool = Field(default=False)
|
341
|
-
|
342
|
-
|
343
|
-
|
324
|
+
outputs: Dict[str, TaskOutput] = Field(default_factory=dict, description="stores node identifier and TaskOutput")
|
325
|
+
concl_template: Optional[Dict[str, Any] | Type[BaseModel]] = Field(default=None, description="stores a format of `concl` either in Pydantic model or JSON dict")
|
326
|
+
concl: Any = Field(default=None, description="store the final result of the entire task graph")
|
344
327
|
|
345
328
|
|
346
|
-
def _save(self, abs_file_path: str = None) -> None:
|
329
|
+
def _save(self, title: str, abs_file_path: str = None) -> None:
|
347
330
|
"""
|
348
331
|
Save the graph image in the local directory.
|
349
332
|
"""
|
350
|
-
|
351
333
|
try:
|
352
334
|
import os
|
353
335
|
project_root = os.path.abspath(os.getcwd())
|
354
|
-
abs_file_path = abs_file_path if abs_file_path else f"{project_root}
|
336
|
+
abs_file_path = abs_file_path if abs_file_path else f"{project_root}/.diagrams"
|
337
|
+
title = title if title else f"vhq-Diagram-{str(self.id)}"
|
355
338
|
|
356
339
|
os.makedirs(abs_file_path, exist_ok=True)
|
357
|
-
plt.savefig(f"{abs_file_path}/{
|
340
|
+
plt.savefig(f"{abs_file_path}/{title}.png")
|
358
341
|
|
359
342
|
except Exception as e:
|
360
343
|
Logger().log(level="error", message=f"Failed to save the graph {str(self.id)}: {str(e)}", color="red")
|
@@ -362,21 +345,38 @@ class TaskGraph(Graph):
|
|
362
345
|
|
363
346
|
def add_task(self, task: Node | Task) -> Node:
|
364
347
|
"""Convert `task` to a Node object and add it to G"""
|
365
|
-
task_node = task if isinstance(task, Node) else Node(task=task)
|
366
|
-
self.add_node(task_node)
|
367
|
-
self.status[task_node.identifier] = TaskStatus.NOT_STARTED
|
368
|
-
return task_node
|
369
348
|
|
349
|
+
if isinstance(task, Node) and task.identifier in self.nodes.keys():
|
350
|
+
return task
|
351
|
+
|
352
|
+
elif isinstance(task, Task):
|
353
|
+
match = []
|
354
|
+
for v in self.nodes.values():
|
355
|
+
if type(v) == dict and v["node"] and v["node"].task == task:
|
356
|
+
match.append(v["node"])
|
357
|
+
elif v.task == task:
|
358
|
+
match.append(v)
|
370
359
|
|
371
|
-
|
372
|
-
|
373
|
-
|
360
|
+
if match:
|
361
|
+
return match[0]
|
362
|
+
else:
|
363
|
+
node = Node(task=task)
|
364
|
+
self.add_node(node)
|
365
|
+
return node
|
366
|
+
|
367
|
+
else:
|
368
|
+
task_node = task if isinstance(task, Node) else Node(task=task)
|
369
|
+
self.add_node(task_node)
|
370
|
+
return task_node
|
371
|
+
|
372
|
+
|
373
|
+
def add_dependency(self, source_node_identifier: str, target_node_identifier: str, **edge_attributes) -> None:
|
374
374
|
"""
|
375
375
|
Add an edge that connect task 1 (source) and task 2 (target) using task_node.name as an identifier
|
376
376
|
"""
|
377
377
|
|
378
378
|
if not edge_attributes:
|
379
|
-
Logger(
|
379
|
+
Logger().log(level="error", message="Edge attributes are missing.", color="red")
|
380
380
|
|
381
381
|
edge = Edge()
|
382
382
|
for k in Edge.model_fields.keys():
|
@@ -386,78 +386,97 @@ class TaskGraph(Graph):
|
|
386
386
|
else:
|
387
387
|
pass
|
388
388
|
|
389
|
-
self.add_edge(
|
390
|
-
|
391
|
-
|
392
|
-
def set_task_status(self, identifier: str, status: TaskStatus) -> None:
|
393
|
-
if identifier in self.status:
|
394
|
-
self.status[identifier] = status
|
395
|
-
else:
|
396
|
-
Logger().log(level="warning", message=f"Task '{identifier}' not found in the graph.", color="yellow")
|
397
|
-
pass
|
389
|
+
self.add_edge(source_node_identifier, target_node_identifier, edge)
|
398
390
|
|
399
391
|
|
400
|
-
def get_task_status(self, identifier):
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
Logger().log(level="warning", message=f"Task '{identifier}' not found in the graph.", color="yellow")
|
392
|
+
def get_task_status(self, identifier: str) -> TaskStatus | None:
|
393
|
+
"""Retrieves the latest status of the given node"""
|
394
|
+
if not identifier or identifier not in self.nodes.keys():
|
395
|
+
Logger().log(level="error", message=f"Task node: {identifier} is not in the graph.", color="red")
|
405
396
|
return None
|
406
397
|
|
398
|
+
return self._return_node_object(identifier).status
|
399
|
+
|
407
400
|
|
408
401
|
def visualize(self, layout: str = None):
|
402
|
+
from matplotlib.lines import Line2D
|
403
|
+
from versionhq.task_graph.colors import white, black, darkgrey, grey, primary, orange, lightgreen, green, darkgreen, darkergreen
|
404
|
+
|
409
405
|
try:
|
410
406
|
pos = nx.drawing.nx_agraph.graphviz_layout(self.graph, prog='dot') # 'dot', 'neato', 'fdp', 'sfdp'
|
411
407
|
except ImportError:
|
412
408
|
pos = nx.spring_layout(self.graph, seed=42) # REFINEME - layout
|
413
409
|
|
414
|
-
node_colors = list()
|
415
|
-
for k, v in self.
|
410
|
+
node_colors, legend_elements = list(), list()
|
411
|
+
for k, v in self.nodes.items():
|
416
412
|
status = self.get_task_status(identifier=k)
|
417
|
-
if
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
413
|
+
node = v if isinstance(v, Node) else v["node"] if hasattr(v, "node") else None
|
414
|
+
if node:
|
415
|
+
task_name = node.task.name if node.task and node.task.name else node.task.description if node.task else None
|
416
|
+
legend_label = f"ID {str(node.identifier)[0: 6]}...: {task_name}" if task_name else f"ID {str(node.identifier)[0: 6]}..."
|
417
|
+
match status:
|
418
|
+
case TaskStatus.NOT_STARTED:
|
419
|
+
node_colors.append(green)
|
420
|
+
legend_elements.append(Line2D([0], [0], marker='o', color='w', label=legend_label, markerfacecolor=green))
|
421
|
+
|
422
|
+
case TaskStatus.IN_PROGRESS:
|
423
|
+
node_colors.append(darkgreen)
|
424
|
+
legend_elements.append(Line2D([0], [0], marker='o', color='w', label=legend_label, markerfacecolor=darkgreen))
|
425
|
+
|
426
|
+
case TaskStatus.WAITING:
|
427
|
+
node_colors.append(lightgreen)
|
428
|
+
legend_elements.append(Line2D([0], [0], marker='o', color='w', label=legend_label, markerfacecolor=lightgreen))
|
429
|
+
|
430
|
+
case TaskStatus.COMPLETED:
|
431
|
+
node_colors.append(darkergreen)
|
432
|
+
legend_elements.append(Line2D([0], [0], marker='o', color='w', label=legend_label, markerfacecolor=darkergreen))
|
433
|
+
|
434
|
+
case TaskStatus.DELAYED:
|
435
|
+
node_colors.append(orange)
|
436
|
+
legend_elements.append(Line2D([0], [0], marker='o', color='w', label=legend_label, markerfacecolor=orange))
|
437
|
+
|
438
|
+
case TaskStatus.ON_HOLD:
|
439
|
+
node_colors.append(grey)
|
440
|
+
legend_elements.append(Line2D([0], [0], marker='o', color='w', label=legend_label, markerfacecolor=grey))
|
441
|
+
|
442
|
+
case TaskStatus.ERROR:
|
443
|
+
node_colors.append(primary)
|
444
|
+
legend_elements.append(Line2D([0], [0], marker='o', color='w', label=legend_label, markerfacecolor=primary))
|
445
|
+
|
446
|
+
case _:
|
447
|
+
node_colors.append(white)
|
448
|
+
legend_elements.append(Line2D([0], [0], marker='o', color='w', label=legend_label, markerfacecolor=white))
|
431
449
|
|
432
450
|
critical_paths, duration, paths = self.find_critical_path()
|
433
|
-
edge_colors = [
|
434
|
-
edge_widths = []
|
451
|
+
edge_colors = [black if (u, v) in zip(critical_paths, critical_paths[1:]) else darkgrey for u, v in self.graph.edges()]
|
452
|
+
edge_widths = [v.weight * 0.5 for v in self.edges.values()]
|
435
453
|
|
436
|
-
for k, v in self.edges.items():
|
437
|
-
|
438
|
-
|
439
|
-
|
454
|
+
# for k, v in self.edges.items():
|
455
|
+
# # edge_weights = nx.get_edge_attributes(self.graph, 'weight')
|
456
|
+
# # edge_colors.append(plt.cm.viridis(v.weight / max(edge_weights.values())))
|
457
|
+
# edge_widths.append(v.weight * 0.5)
|
440
458
|
|
441
459
|
nx.draw(
|
442
460
|
self.graph, pos,
|
443
461
|
with_labels=True,
|
444
|
-
node_size=
|
462
|
+
node_size=600,
|
445
463
|
node_color=node_colors,
|
446
|
-
font_size=
|
447
|
-
font_color=
|
464
|
+
font_size=8,
|
465
|
+
font_color=darkgrey,
|
448
466
|
edge_color=edge_colors,
|
449
467
|
width=edge_widths,
|
450
468
|
arrows=True,
|
451
|
-
arrowsize=
|
469
|
+
arrowsize=15,
|
452
470
|
arrowstyle='-|>'
|
453
471
|
)
|
454
472
|
|
455
473
|
edge_labels = nx.get_edge_attributes(G=self.graph, name="edges")
|
456
474
|
nx.draw_networkx_edge_labels(self.graph, pos, edge_labels=edge_labels)
|
457
475
|
|
458
|
-
plt.
|
459
|
-
self.
|
460
|
-
|
476
|
+
plt.legend(handles=legend_elements, loc='lower right')
|
477
|
+
plt.title(f"vhq-Diagram {str(self.id)}")
|
478
|
+
self._save(title=f"vhq-Diagram {str(self.id)}")
|
479
|
+
plt.show(block=False)
|
461
480
|
|
462
481
|
|
463
482
|
def activate(self, target_node_identifier: Optional[str] = None) -> Tuple[TaskOutput | None, Dict[str, TaskOutput]]:
|
@@ -465,6 +484,9 @@ class TaskGraph(Graph):
|
|
465
484
|
Starts to execute all nodes in the graph or a specific node if the target is given, following the given conditons of the edge obeject.
|
466
485
|
Then returns tuple of the last task output and all task outputs (self.outputs)
|
467
486
|
"""
|
487
|
+
|
488
|
+
Logger().log(color="blue", message=f"Start to activate the graph: {str(self.id)}", level="info")
|
489
|
+
|
468
490
|
if target_node_identifier:
|
469
491
|
if not [k for k in self.nodes.keys() if k == target_node_identifier]:
|
470
492
|
Logger().log(level="error", message=f"The node {str(target_node_identifier)} is not in the graph.", color="red")
|
@@ -483,7 +505,6 @@ class TaskGraph(Graph):
|
|
483
505
|
if len([item for item in edge_status if item["dep_met"] == True]) == len(sources):
|
484
506
|
res = node.handle_task_execution()
|
485
507
|
self.outputs.update({ target_node_identifier: res })
|
486
|
-
self.status.update({ target_node_identifier: edge.target.status })
|
487
508
|
|
488
509
|
return res, self.outputs
|
489
510
|
|
@@ -502,15 +523,13 @@ class TaskGraph(Graph):
|
|
502
523
|
if end_nodes and len([node for node in end_nodes if node.status == TaskStatus.COMPLETED]) == len(end_nodes):
|
503
524
|
if critical_end_node:
|
504
525
|
return critical_end_node.task.output, self.outputs
|
505
|
-
|
506
526
|
else:
|
507
|
-
return [v.task.output
|
527
|
+
return [v.task.output for k, v in end_nodes.items()][0], self.outputs
|
508
528
|
|
509
529
|
# Else, execute nodes connected with the critical_path
|
510
530
|
elif critical_path:
|
511
531
|
for item in critical_path:
|
512
532
|
edge = [v for k, v in self.edges.items() if item in k]
|
513
|
-
|
514
533
|
if edge:
|
515
534
|
edge = edge[0]
|
516
535
|
|
@@ -521,29 +540,24 @@ class TaskGraph(Graph):
|
|
521
540
|
res = edge.activate()
|
522
541
|
node_identifier = edge.target.identifier
|
523
542
|
self.outputs.update({ node_identifier: res })
|
524
|
-
self.status.update({ node_identifier: edge.target.status })
|
525
543
|
|
526
544
|
if not res and start_nodes:
|
527
545
|
for node in start_nodes:
|
528
546
|
res = node.handle_task_execution()
|
529
547
|
self.outputs.update({ node.identifier: res })
|
530
|
-
self.status.update({ node.identifier: node.status })
|
531
548
|
|
532
|
-
#
|
549
|
+
# If no critical paths in the graph, simply start from the start nodes.
|
533
550
|
elif start_nodes:
|
534
551
|
for node in start_nodes:
|
535
552
|
res = node.handle_task_execution()
|
536
553
|
self.outputs.update({ node.identifier: res })
|
537
|
-
self.status.update({ node.identifier: node.status })
|
538
|
-
|
539
554
|
|
540
|
-
#
|
555
|
+
# If none of above is applicable, try to activate all the edges.
|
541
556
|
else:
|
542
557
|
for k, edge in self.edges.items():
|
543
558
|
res = edge.activate()
|
544
559
|
node_identifier = edge.target.identifier
|
545
560
|
self.outputs.update({ node_identifier: res })
|
546
|
-
self.status.update({ node_identifier: edge.target.status })
|
547
561
|
|
548
562
|
# last_task_output = [v for v in self.outputs.values()][len([v for v in self.outputs.values()]) - 1] if [v for v in self.outputs.values()] else None
|
549
563
|
return res, self.outputs
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: versionhq
|
3
|
-
Version: 1.2.1.
|
3
|
+
Version: 1.2.1.7
|
4
4
|
Summary: An agentic orchestration framework for building agent networks that handle task automation.
|
5
5
|
Author-email: Kuriko Iwai <kuriko@versi0n.io>
|
6
6
|
License: MIT License
|
@@ -89,7 +89,7 @@ Requires-Dist: pygraphviz>=1.14; extra == "pygraphviz"
|
|
89
89
|

|
90
90
|
|
91
91
|
|
92
|
-
Agentic orchestration framework for multi-agent networks and task
|
92
|
+
Agentic orchestration framework for multi-agent networks and task graphs for complex task automation.
|
93
93
|
|
94
94
|
**Visit:**
|
95
95
|
|
@@ -181,11 +181,11 @@ node_c = task_graph.add_task(task=task_c)
|
|
181
181
|
|
182
182
|
task_graph.add_dependency(
|
183
183
|
node_a.identifier, node_b.identifier,
|
184
|
-
|
184
|
+
dependency_type=vhq.DependencyType.FINISH_TO_START, weight=5, description="B depends on A"
|
185
185
|
)
|
186
186
|
task_graph.add_dependency(
|
187
187
|
node_a.identifier, node_c.identifier,
|
188
|
-
|
188
|
+
dependency_type=vhq.DependencyType.FINISH_TO_FINISH, lag=1, required=False, weight=3
|
189
189
|
)
|
190
190
|
|
191
191
|
# To visualize the graph:
|
@@ -361,7 +361,7 @@ Tasks can be delegated to a manager, peers within the agent network, or a comple
|
|
361
361
|
|
362
362
|
**Workflow, Task Graph**
|
363
363
|
|
364
|
-
* [NetworkX](https://networkx.org/documentation/stable/reference/introduction.html): A Python package to analyze, create, and manipulate complex graph networks.
|
364
|
+
* [NetworkX](https://networkx.org/documentation/stable/reference/introduction.html): A Python package to analyze, create, and manipulate complex graph networks. Ref. [Gallary](https://networkx.org/documentation/latest/auto_examples/index.html)
|
365
365
|
* [Matplotlib](https://matplotlib.org/stable/index.html): For graph visualization.
|
366
366
|
* [Graphviz](https://graphviz.org/about/): For graph visualization.
|
367
367
|
|
@@ -397,7 +397,7 @@ Tasks can be delegated to a manager, peers within the agent network, or a comple
|
|
397
397
|
.github
|
398
398
|
└── workflows/ # Github actions
|
399
399
|
│
|
400
|
-
docs/ # Documentation
|
400
|
+
docs/ # Documentation
|
401
401
|
mkdocs.yml # MkDocs config
|
402
402
|
│
|
403
403
|
src/
|
@@ -413,12 +413,13 @@ src/
|
|
413
413
|
│ └── llm/
|
414
414
|
│ └── ...
|
415
415
|
│
|
416
|
-
└──
|
416
|
+
└── .diagrams/ [.gitignore] # Local directory to store graph diagrams
|
417
417
|
│
|
418
|
-
└── .logs/
|
418
|
+
└── .logs/ [.gitignore] # Local directory to store error/warning logs for debugging
|
419
419
|
│
|
420
420
|
│
|
421
421
|
pyproject.toml # Project config
|
422
|
+
.env.sample # sample .env file
|
422
423
|
|
423
424
|
```
|
424
425
|
|
@@ -475,16 +476,7 @@ pyproject.toml # Project config
|
|
475
476
|
|
476
477
|
### Adding env secrets to .env file
|
477
478
|
|
478
|
-
Create `.env` file in the project root and add following
|
479
|
-
|
480
|
-
```
|
481
|
-
OPENAI_API_KEY=your-openai-api-key
|
482
|
-
GEMINI_API_KEY=your-gemini-api-key
|
483
|
-
LITELLM_API_KEY=your-litellm-api-key
|
484
|
-
COMPOSIO_API_KEY=your-composio-api-key
|
485
|
-
COMPOSIO_CLI_KEY=your-composio-cli-key
|
486
|
-
[OTHER_LLM_INTERFACE_PROVIDER_OF_YOUR_CHOICE]_API_KEY=your-api-key
|
487
|
-
```
|
479
|
+
Create `.env` file in the project root and add secret vars following `.env.sample` file.
|
488
480
|
|
489
481
|
|
490
482
|
<hr />
|
@@ -1,4 +1,4 @@
|
|
1
|
-
versionhq/__init__.py,sha256=
|
1
|
+
versionhq/__init__.py,sha256=S0jSgBSycw_tEb8kM_GR2-pM-ws0e1ScMMK7LWIE4eI,2882
|
2
2
|
versionhq/_utils/__init__.py,sha256=dzoZr4cBlh-2QZuPzTdehPUCe9lP1dmRtauD7qTjUaA,158
|
3
3
|
versionhq/_utils/i18n.py,sha256=TwA_PnYfDLA6VqlUDPuybdV9lgi3Frh_ASsb_X8jJo8,1483
|
4
4
|
versionhq/_utils/logger.py,sha256=zgogTwAY-ujDLrdryAKhdtoaNe1nOFajmEN0V8aMR34,3155
|
@@ -44,14 +44,16 @@ versionhq/storage/task_output_storage.py,sha256=E1t_Fkt78dPYIOl3MP7LfQ8oGtjlzxBu
|
|
44
44
|
versionhq/storage/utils.py,sha256=ByYXPoEIGJYLUqz-DWjbCAnneNrH1otiYbp12SCILpM,747
|
45
45
|
versionhq/task/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
46
46
|
versionhq/task/evaluate.py,sha256=WdUgjbZL62XrxyWe5MTz29scfzwmuAHGxJ7GvAB8Fmk,3954
|
47
|
-
versionhq/task/formation.py,sha256=
|
47
|
+
versionhq/task/formation.py,sha256=WH604q9bRmWH7KQCrk2qKJwisCopYX5CjJvsj4TgFjI,6894
|
48
48
|
versionhq/task/formatter.py,sha256=N8Kmk9vtrMtBdgJ8J7RmlKNMdZWSmV8O1bDexmCWgU0,643
|
49
49
|
versionhq/task/log_handler.py,sha256=LT7YnO7gcPR9IZS7eRvMjnHh8crMBFtqduxd8dxIbkk,1680
|
50
|
-
versionhq/task/model.py,sha256=
|
50
|
+
versionhq/task/model.py,sha256=b2d3mpac1Mah_21vk1TfcvBDiz94-3ZorCoK8pfIxkE,28530
|
51
51
|
versionhq/task/structured_response.py,sha256=4q-hQPu7oMMHHXEzh9YW4SJ7N5eCZ7OfZ65juyl_jCI,5000
|
52
52
|
versionhq/task/TEMPLATES/Description.py,sha256=V-4kh8xpQTKOcDMi2xnuP-fcNk6kuoz1_5tYBlDLQWQ,420
|
53
53
|
versionhq/task_graph/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
54
|
-
versionhq/task_graph/
|
54
|
+
versionhq/task_graph/colors.py,sha256=naJCx4Vho4iuJtbW8USUXb-M5uYvd5ds2p8qbjUfRus,669
|
55
|
+
versionhq/task_graph/draft.py,sha256=nALpwFIDEEhij64ezMRKyNhmyyiXIhzA-6wpcU35xIs,4772
|
56
|
+
versionhq/task_graph/model.py,sha256=ZXpuG6f1OZBrGV0Ai1kBuRDNSlwj17WH5GAuWvTWjFQ,23545
|
55
57
|
versionhq/tool/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
56
58
|
versionhq/tool/cache_handler.py,sha256=iL8FH7X0G-cdT0uhJwzuhLDaadTXOdfybZcDy151-es,1085
|
57
59
|
versionhq/tool/composio_tool.py,sha256=38mEiVvTkuw1BLD233Bl1Gwxbpss1yfQiZLTWwX6BdA,8648
|
@@ -59,8 +61,8 @@ versionhq/tool/composio_tool_vars.py,sha256=FvBuEXsOQUYnN7RTFxT20kAkiEYkxWKkiVtg
|
|
59
61
|
versionhq/tool/decorator.py,sha256=C4ZM7Xi2gwtEMaSeRo-geo_g_MAkY77WkSLkAuY0AyI,1205
|
60
62
|
versionhq/tool/model.py,sha256=PO4zNWBZcJhYVur381YL1dy6zqurio2jWjtbxOxZMGI,12194
|
61
63
|
versionhq/tool/tool_handler.py,sha256=2m41K8qo5bGCCbwMFferEjT-XZ-mE9F0mDUOBkgivOI,1416
|
62
|
-
versionhq-1.2.1.
|
63
|
-
versionhq-1.2.1.
|
64
|
-
versionhq-1.2.1.
|
65
|
-
versionhq-1.2.1.
|
66
|
-
versionhq-1.2.1.
|
64
|
+
versionhq-1.2.1.7.dist-info/LICENSE,sha256=cRoGGdM73IiDs6nDWKqPlgSv7aR4n-qBXYnJlCMHCeE,1082
|
65
|
+
versionhq-1.2.1.7.dist-info/METADATA,sha256=-7ao4UNVn_g-zS5xNXuzJ-XFrbSCZ2dkcXc9IGVO-gA,22226
|
66
|
+
versionhq-1.2.1.7.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
67
|
+
versionhq-1.2.1.7.dist-info/top_level.txt,sha256=DClQwxDWqIUGeRJkA8vBlgeNsYZs4_nJWMonzFt5Wj0,10
|
68
|
+
versionhq-1.2.1.7.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|