versionhq 1.1.7.5__py3-none-any.whl → 1.1.7.8__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 CHANGED
@@ -17,7 +17,7 @@ from versionhq.team.model import Team, TeamOutput
17
17
  from versionhq.tool.model import Tool
18
18
 
19
19
 
20
- __version__ = "1.1.7.5"
20
+ __version__ = "1.1.7.8"
21
21
  __all__ = [
22
22
  "Agent",
23
23
  "Customer",
@@ -2,9 +2,7 @@ from typing import Any, Dict, Type
2
2
  from pydantic import BaseModel
3
3
 
4
4
 
5
- def process_config(
6
- values_to_update: Dict[str, Any], model_class: Type[BaseModel]
7
- ) -> Dict[str, Any]:
5
+ def process_config(values_to_update: Dict[str, Any], model_class: Type[BaseModel]) -> Dict[str, Any]:
8
6
  """
9
7
  Process the config dictionary and update the values accordingly.
10
8
  Refer to the Pydantic model class for field validation.
@@ -15,7 +13,7 @@ def process_config(
15
13
  else:
16
14
  return values_to_update
17
15
 
18
- # copy values from config to the model's attributes if the attribute isn't already set.
16
+
19
17
  for key, value in config.items():
20
18
  if key not in model_class.model_fields or values_to_update.get(key) is not None:
21
19
  continue
versionhq/agent/model.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  import uuid
3
3
  from abc import ABC
4
- from typing import Any, Dict, List, Optional, TypeVar, Union
4
+ from typing import Any, Dict, List, Optional, TypeVar
5
5
  from dotenv import load_dotenv
6
6
  from pydantic import UUID4, BaseModel, Field, InstanceOf, PrivateAttr, model_validator, field_validator
7
7
  from pydantic_core import PydanticCustomError
@@ -22,7 +22,7 @@ load_dotenv(override=True)
22
22
  T = TypeVar("T", bound="Agent")
23
23
 
24
24
 
25
- # def _format_answer(agent, answer: str) -> Union[AgentAction, AgentFinish]:
25
+ # def _format_answer(agent, answer: str) -> AgentAction | AgentFinish:
26
26
  # return AgentParser(agent=agent).parse(answer)
27
27
 
28
28
  # def mock_agent_ops_provider():
@@ -85,7 +85,7 @@ class Agent(ABC, BaseModel):
85
85
  """
86
86
 
87
87
  __hash__ = object.__hash__
88
- _logger: Logger = PrivateAttr(default_factory=lambda: Logger(verbose=False))
88
+ _logger: Logger = PrivateAttr(default_factory=lambda: Logger(verbose=True))
89
89
  _rpm_controller: Optional[RPMController] = PrivateAttr(default=None)
90
90
  _request_within_rpm_limit: Any = PrivateAttr(default=None)
91
91
  _token_process: TokenProcess = PrivateAttr(default_factory=TokenProcess)
@@ -111,8 +111,8 @@ class Agent(ABC, BaseModel):
111
111
  step_callback: Optional[Any] = Field(default=None,description="Callback to be executed after each step of the agent execution")
112
112
 
113
113
  # llm settings cascaded to the LLM model
114
- llm: Union[str, InstanceOf[LLM], Any] = Field(default=None)
115
- function_calling_llm: Union[str, InstanceOf[LLM], Any] = Field(default=None)
114
+ llm: str | InstanceOf[LLM] | Any = Field(default=None)
115
+ function_calling_llm: str | InstanceOf[LLM] | Any = Field(default=None)
116
116
  respect_context_window: bool = Field(default=True,description="Keep messages under the context window size by summarizing content")
117
117
  max_tokens: Optional[int] = Field(default=None, description="max. number of tokens for the agent's execution")
118
118
  max_execution_time: Optional[int] = Field(default=None, description="max. execution time for an agent to execute a task")
@@ -327,7 +327,7 @@ class Agent(ABC, BaseModel):
327
327
  messages = []
328
328
  messages.append({"role": "user", "content": prompts}) #! REFINEME
329
329
  messages.append({"role": "assistant", "content": self.backstory})
330
- print("Messages sent to the model:", messages)
330
+ self._logger.log(level="info", message=f"Messages sent to the model: {messages}", color="blue")
331
331
 
332
332
  callbacks = kwargs.get("callbacks", None)
333
333
 
@@ -338,7 +338,7 @@ class Agent(ABC, BaseModel):
338
338
  callbacks=callbacks,
339
339
  )
340
340
  task_execution_counter += 1
341
- print("Agent's #1 res: ", response)
341
+ self._logger.log(level="info", message=f"Agent's first response: {response}", color="blue")
342
342
 
343
343
  if (response is None or response == "") and task_execution_counter < self.max_retry_limit:
344
344
  while task_execution_counter <= self.max_retry_limit:
@@ -349,10 +349,10 @@ class Agent(ABC, BaseModel):
349
349
  callbacks=callbacks,
350
350
  )
351
351
  task_execution_counter += 1
352
- print(f"Agent's #{task_execution_counter} res: ", response)
352
+ self._logger.log(level="info", message=f"Agent's next response: {response}", color="blue")
353
353
 
354
354
  elif response is None or response == "":
355
- print("Received None or empty response from LLM call.")
355
+ self._logger.log(level="error", message="Received None or empty response from the model", color="red")
356
356
  raise ValueError("Invalid response from LLM call - None or empty.")
357
357
 
358
358
  return {"output": response.output if hasattr(response, "output") else response}
versionhq/agent/parser.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import re
2
- from typing import Any, Union
2
+ from typing import Any
3
3
  from json_repair import repair_json
4
4
 
5
5
  from versionhq._utils.i18n import I18N
@@ -70,7 +70,7 @@ class AgentParser:
70
70
  def __init__(self, agent: Any):
71
71
  self.agent = agent
72
72
 
73
- def parse(self, text: str) -> Union[AgentAction, AgentFinish]:
73
+ def parse(self, text: str) -> AgentAction | AgentFinish:
74
74
  thought = self._extract_thought(text)
75
75
  includes_answer = FINAL_ANSWER_ACTION in text
76
76
  regex = (
@@ -1,8 +1,8 @@
1
1
  import uuid
2
2
  from abc import ABC
3
3
  from datetime import date, datetime, time, timedelta
4
- from typing import Any, Dict, List, Union, Callable, Type, Optional, get_args, get_origin
5
- from pydantic import UUID4, InstanceOf, BaseModel, ConfigDict, Field, create_model, field_validator, model_validator
4
+ from typing import Any, Dict, List, Callable, Type, Optional, get_args, get_origin
5
+ from pydantic import UUID4, InstanceOf, BaseModel, ConfigDict, Field, field_validator, model_validator
6
6
  from pydantic_core import PydanticCustomError
7
7
 
8
8
  from versionhq.clients.product.model import Product
@@ -12,7 +12,7 @@ from versionhq.team.model import Team
12
12
 
13
13
 
14
14
  class ScoreFormat:
15
- def __init__(self, rate: Union[float, int] = 0, weight: int = 1):
15
+ def __init__(self, rate: float | int = 0, weight: int = 1):
16
16
  self.rate = rate
17
17
  self.weight = weight
18
18
  self.aggregate = rate * weight
@@ -39,7 +39,7 @@ class Score:
39
39
 
40
40
 
41
41
  def result(self) -> int:
42
- aggregate_score = self.brand_tone.aggregate + self.audience.aggregate + self.track_record.aggregate
42
+ aggregate_score = int(self.brand_tone.aggregate) + int(self.audience.aggregate) + int(self.track_record.aggregate)
43
43
  denominator = self.brand_tone.weight + self.audience.weight + self.track_record.weight
44
44
 
45
45
  for k, v in self.kwargs.items():
@@ -57,11 +57,12 @@ class MessagingComponent(ABC, BaseModel):
57
57
  layer_id: int = Field(default=0, description="add id of the layer: 0, 1, 2")
58
58
  message: str = Field(default=None, max_length=1024, description="text message content to be sent")
59
59
  interval: Optional[str] = Field(
60
- default=None,description="interval to move on to the next layer. if this is the last layer, set as `None`")
61
- score: Union[float, InstanceOf[Score]] = Field(default=None)
60
+ default=None, description="interval to move on to the next layer. if this is the last layer, set as `None`"
61
+ )
62
+ score: float | InstanceOf[Score] = Field(default=None)
62
63
 
63
64
 
64
- def store_scoring_result(self, scoring_subject: str, score_raw: Union[int, Score, ScoreFormat] = None):
65
+ def store_scoring_result(self, scoring_subject: str, score_raw: int | Score | ScoreFormat = None):
65
66
  """
66
67
  Set up the `score` field
67
68
  """
@@ -109,8 +110,11 @@ class MessagingWorkflow(ABC, BaseModel):
109
110
  product: InstanceOf[Product] = Field(default=None)
110
111
  customer: InstanceOf[Customer] = Field(default=None)
111
112
 
112
- metrics: Union[List[Dict[str, Any]], List[str]] = Field(
113
- default=None, max_length=256, description="store metrics that used to predict and track the performance of this workflow.")
113
+ metrics: List[Dict[str, Any]] | List[str] = Field(
114
+ default=None,
115
+ max_length=256,
116
+ description="store metrics that used to predict and track the performance of this workflow."
117
+ )
114
118
 
115
119
 
116
120
  @property
versionhq/llm/model.py CHANGED
@@ -7,7 +7,7 @@ import litellm
7
7
  from dotenv import load_dotenv
8
8
  from litellm import get_supported_openai_params
9
9
  from contextlib import contextmanager
10
- from typing import Any, Dict, List, Optional, Union
10
+ from typing import Any, Dict, List, Optional
11
11
 
12
12
  from versionhq.llm.llm_vars import LLM_CONTEXT_WINDOW_SIZES
13
13
  from versionhq.task import TaskOutputFormat
@@ -103,7 +103,7 @@ class LLM:
103
103
  def __init__(
104
104
  self,
105
105
  model: str,
106
- timeout: Optional[Union[float, int]] = None,
106
+ timeout: Optional[float | int] = None,
107
107
  max_tokens: Optional[int] = None,
108
108
  max_completion_tokens: Optional[int] = None,
109
109
  context_window_size: Optional[int] = DEFAULT_CONTEXT_WINDOW,
@@ -111,7 +111,7 @@ class LLM:
111
111
  temperature: Optional[float] = None,
112
112
  top_p: Optional[float] = None,
113
113
  n: Optional[int] = None,
114
- stop: Optional[Union[str, List[str]]] = None,
114
+ stop: Optional[str | List[str]] = None,
115
115
  presence_penalty: Optional[float] = None,
116
116
  frequency_penalty: Optional[float] = None,
117
117
  logit_bias: Optional[Dict[int, float]] = None,
File without changes
@@ -0,0 +1,141 @@
1
+ import appdirs
2
+ import os
3
+ import json
4
+ import sqlite3
5
+ import datetime
6
+ from typing import Any, Dict, List, Optional
7
+ from dotenv import load_dotenv
8
+ from pathlib import Path
9
+
10
+ from versionhq._utils.logger import Logger
11
+
12
+ load_dotenv(override=True)
13
+
14
+
15
+ def fetch_db_storage_path():
16
+ project_directory_name = os.environ.get("STORAGE_DIR", Path.cwd().name)
17
+ app_author = "versionhq"
18
+ data_dir = Path(appdirs.user_data_dir(project_directory_name, app_author))
19
+ data_dir.mkdir(parents=True, exist_ok=True)
20
+ return data_dir
21
+
22
+ storage_path = fetch_db_storage_path()
23
+ default_db_name = "task_outputs"
24
+
25
+
26
+ class TaskOutputSQLiteStorage:
27
+ """
28
+ An SQLite storage class to handle storing task outputs.
29
+ """
30
+
31
+ def __init__(self, db_path: str = f"{storage_path}/{default_db_name}.db") -> None:
32
+ self.db_path = db_path
33
+ self._logger = Logger(verbose=True)
34
+ self._initialize_db()
35
+
36
+
37
+ def _initialize_db(self):
38
+ """
39
+ Initializes the SQLite database and creates LTM table.
40
+ """
41
+
42
+ try:
43
+ with sqlite3.connect(self.db_path) as conn:
44
+ cursor = conn.cursor()
45
+ cursor.execute(
46
+ """
47
+ CREATE TABLE IF NOT EXISTS task_outputs (
48
+ task_id TEXT PRIMARY KEY,
49
+ output JSON,
50
+ task_index INTEGER,
51
+ inputs JSON,
52
+ was_replayed BOOLEAN,
53
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
54
+ )
55
+ """
56
+ )
57
+ conn.commit()
58
+
59
+ except sqlite3.Error as e:
60
+ self._logger.log(level="error", message=f"DATABASE INITIALIZATION ERROR: {e}", color="red")
61
+
62
+
63
+ def add(self, task, output: Dict[str, Any], task_index: int, was_replayed: bool = False, inputs: Dict[str, Any] = {}):
64
+ try:
65
+ with sqlite3.connect(self.db_path) as conn:
66
+ cursor = conn.cursor()
67
+ cursor.execute(
68
+ """INSERT OR REPLACE INTO task_outputs
69
+ (task_id, output, task_index, inputs, was_replayed, timestamp)
70
+ VALUES (?, ?, ?, ?, ?, ?)
71
+ """,
72
+ (str(task.id), json.dumps(output), task_index, json.dumps(inputs), was_replayed, datetime.datetime.now())
73
+ )
74
+ conn.commit()
75
+
76
+ except sqlite3.Error as e:
77
+ self._logger.log(level="error", message=f"SAVING TASK OUTPUTS ERROR: {e}", color="red")
78
+
79
+
80
+ def update(self, task_index: int, **kwargs):
81
+ try:
82
+ with sqlite3.connect(self.db_path) as conn:
83
+ cursor = conn.cursor()
84
+
85
+ fields, values = [], []
86
+ for k, v in kwargs.items():
87
+ fields.append(f"{k} = ?")
88
+ values.append(json.dumps(v) if isinstance(v, dict) else v)
89
+
90
+ query = f"UPDATE latest_kickoff_task_outputs SET {', '.join(fields)} WHERE task_index = ?" # nosec
91
+ values.append(task_index)
92
+ cursor.execute(query, tuple(values))
93
+ conn.commit()
94
+
95
+ if cursor.rowcount == 0:
96
+ self._logger.log(
97
+ level="info", message=f"No row found with task_index {task_index}. No update performed.", color="yellow",
98
+ )
99
+
100
+ except sqlite3.Error as e:
101
+ self._logger.log(level="error", message=f"UPDATE TASK OUTPUTS ERROR: {e}", color="red")
102
+
103
+
104
+ def load(self) -> Optional[List[Dict[str, Any]]]:
105
+ try:
106
+ with sqlite3.connect(self.db_path) as conn:
107
+ cursor = conn.cursor()
108
+ cursor.execute("""
109
+ SELECT *
110
+ FROM task_outputs
111
+ ORDER BY task_index
112
+ """)
113
+
114
+ rows = cursor.fetchall()
115
+ results = []
116
+ for row in rows:
117
+ result = {
118
+ "task_id": row[0],
119
+ "output": json.loads(row[1]),
120
+ "task_index": row[2],
121
+ "inputs": json.loads(row[3]),
122
+ "was_replayed": row[4],
123
+ "timestamp": row[5],
124
+ }
125
+ results.append(result)
126
+ return results
127
+
128
+ except sqlite3.Error as e:
129
+ self._logger.log(level="error", message=f"LOADING TASK OUTPUTS ERROR: {e}", color="red")
130
+ return None
131
+
132
+
133
+ def delete_all(self):
134
+ try:
135
+ with sqlite3.connect(self.db_path) as conn:
136
+ cursor = conn.cursor()
137
+ cursor.execute("DELETE FROM task_outputs")
138
+ conn.commit()
139
+
140
+ except sqlite3.Error as e:
141
+ self._logger.log(level="error", message=f"ERROR: Failed to delete all: {e}", color="red")
@@ -0,0 +1,59 @@
1
+ from datetime import datetime
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+ from versionhq.storage.task_output_storage import TaskOutputSQLiteStorage
7
+
8
+
9
+ class ExecutionLog(BaseModel):
10
+ task_id: str
11
+ output: Dict[str, Any]
12
+ timestamp: datetime = Field(default_factory=datetime.now)
13
+ task_index: int
14
+ inputs: Dict[str, Any] = Field(default_factory=dict)
15
+ was_replayed: bool = False
16
+
17
+ def __getitem__(self, key: str) -> Any:
18
+ return getattr(self, key)
19
+
20
+
21
+
22
+ class TaskOutputStorageHandler:
23
+
24
+ def __init__(self):
25
+ self.storage = TaskOutputSQLiteStorage()
26
+
27
+
28
+ def update(self, task, task_index: int, was_replayed: bool = False, inputs: Dict[str, Any] = {}) -> None:
29
+ """
30
+ task: task instance
31
+ """
32
+ saved_outputs = self.load()
33
+ if saved_outputs is None:
34
+ raise ValueError("Logs cannot be None")
35
+
36
+ self.add(task, task_index, was_replayed, inputs)
37
+
38
+
39
+ def add(self, task, task_index: int, was_replayed: bool = False, inputs: Dict[str, Any] = {}) -> None:
40
+ from versionhq.task.model import Task
41
+
42
+ output_to_store = dict()
43
+
44
+ if isinstance(task, Task):
45
+ output_to_store = dict(
46
+ description=str(task.description),
47
+ raw=str(task.output.raw),
48
+ responsible_agent=str(task.processed_by_agents),
49
+ )
50
+
51
+ self.storage.add(task=task, output=output_to_store, task_index=task_index, was_replayed=was_replayed, inputs=inputs)
52
+
53
+
54
+ def reset(self) -> None:
55
+ self.storage.delete_all()
56
+
57
+
58
+ def load(self) -> Optional[List[Dict[str, Any]]]:
59
+ return self.storage.load()
versionhq/task/model.py CHANGED
@@ -3,15 +3,17 @@ import threading
3
3
  import uuid
4
4
  from concurrent.futures import Future
5
5
  from hashlib import md5
6
- from typing import Any, Dict, List, Set, Optional, Tuple, Callable, Union, Type
6
+ from typing import Any, Dict, List, Set, Optional, Tuple, Callable, Type
7
7
  from typing_extensions import Annotated
8
8
 
9
- from pydantic import UUID4, BaseModel, Field, PrivateAttr, field_validator, model_validator, create_model
9
+ from pydantic import UUID4, BaseModel, Field, PrivateAttr, field_validator, model_validator, create_model, InstanceOf
10
10
  from pydantic_core import PydanticCustomError
11
11
 
12
12
  from versionhq._utils.process_config import process_config
13
13
  from versionhq.task import TaskOutputFormat
14
+ from versionhq.task.log_handler import TaskOutputStorageHandler
14
15
  from versionhq.tool.model import Tool, ToolCalled
16
+ from versionhq._utils.logger import Logger
15
17
 
16
18
 
17
19
  class ResponseField(BaseModel):
@@ -47,7 +49,7 @@ class ResponseField(BaseModel):
47
49
  return value
48
50
 
49
51
 
50
- def create_pydantic_model(self, result: Dict, base_model: Union[BaseModel | Any]) -> Any:
52
+ def create_pydantic_model(self, result: Dict, base_model: InstanceOf[BaseModel] | Any) -> Any:
51
53
  for k, v in result.items():
52
54
  if k is not self.title:
53
55
  pass
@@ -59,20 +61,6 @@ class ResponseField(BaseModel):
59
61
  return base_model
60
62
 
61
63
 
62
- class AgentOutput(BaseModel):
63
- """
64
- Keep adding agents' learning and recommendation and store it in `pydantic` field of `TaskOutput` class.
65
- Since the TaskOutput class has `agent` field, we don't add any info on the agent that handled the task.
66
- """
67
- customer_id: str = Field(default=None, max_length=126, description="customer uuid")
68
- customer_analysis: str = Field(default=None, max_length=256, description="analysis of the customer")
69
- product_overview: str = Field(default=None, max_length=256, description="analysis of the client's business")
70
- usp: str = Field()
71
- cohort_timeframe: int = Field(default=None, max_length=256, description="suitable cohort timeframe in days")
72
- kpi_metrics: List[str] = Field(default=list, description="Ideal KPIs to be tracked")
73
- assumptions: List[Dict[str, Any]] = Field(default=list, description="assumptions to test")
74
-
75
-
76
64
 
77
65
  class TaskOutput(BaseModel):
78
66
  """
@@ -82,7 +70,7 @@ class TaskOutput(BaseModel):
82
70
 
83
71
  task_id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True, description="store Task ID")
84
72
  raw: str = Field(default="", description="Raw output of the task")
85
- json_dict: Union[Dict[str, Any]] = Field(default=None, description="`raw` converted to dictionary")
73
+ json_dict: Dict[str, Any] = Field(default=None, description="`raw` converted to dictionary")
86
74
  pydantic: Optional[Any] = Field(default=None, description="`raw` converted to the abs. pydantic model")
87
75
 
88
76
  def __str__(self) -> str:
@@ -133,11 +121,13 @@ class Task(BaseModel):
133
121
  """
134
122
 
135
123
  __hash__ = object.__hash__
124
+ _original_description: str = PrivateAttr(default=None)
125
+ _logger: Logger = PrivateAttr()
126
+ _task_output_handler = TaskOutputStorageHandler()
136
127
 
137
128
  id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True, description="unique identifier for the object, not set by user")
138
129
  name: Optional[str] = Field(default=None)
139
130
  description: str = Field(description="Description of the actual task")
140
- _original_description: str = PrivateAttr(default=None)
141
131
 
142
132
  # output
143
133
  expected_output_json: bool = Field(default=True)
@@ -161,7 +151,7 @@ class Task(BaseModel):
161
151
  callback_kwargs: Optional[Dict[str, Any]] = Field(default_factory=dict, description="kwargs for the callback when the callback is callable")
162
152
 
163
153
  # recording
164
- processed_by_agents: Set[str] = Field(default_factory=set)
154
+ processed_by_agents: Set[str] = Field(default_factory=set, description="store responsible agents' roles")
165
155
  used_tools: int = 0
166
156
  tools_errors: int = 0
167
157
  delegations: int = 0
@@ -372,7 +362,7 @@ Your outputs MUST adhere to the following format and should NOT include any irre
372
362
 
373
363
 
374
364
  # task execution
375
- def execute_sync(self, agent, context: Optional[str] = None) -> TaskOutput:
365
+ def execute_sync(self, agent, context: Optional[str] = None, tools: Optional[List[Any]] = None) -> TaskOutput:
376
366
  """
377
367
  Execute the task synchronously.
378
368
  When the task has context, make sure we have executed all the tasks in the context first.
@@ -386,7 +376,7 @@ Your outputs MUST adhere to the following format and should NOT include any irre
386
376
  return self._execute_core(agent, context)
387
377
 
388
378
 
389
- def execute_async(self, agent, context: Optional[str] = None) -> Future[TaskOutput]:
379
+ def execute_async(self, agent, context: Optional[str] = None, tools: Optional[List[Any]] = None) -> Future[TaskOutput]:
390
380
  """
391
381
  Execute the task asynchronously.
392
382
  """
@@ -395,21 +385,21 @@ Your outputs MUST adhere to the following format and should NOT include any irre
395
385
  threading.Thread(
396
386
  daemon=True,
397
387
  target=self._execute_task_async,
398
- args=(agent, context, future),
388
+ args=(agent, context, tools, future),
399
389
  ).start()
400
390
  return future
401
391
 
402
392
 
403
- def _execute_task_async(self, agent, context: Optional[str], future: Future[TaskOutput]) -> None:
393
+ def _execute_task_async(self, agent, context: Optional[str], tools: Optional[List[Any]], future: Future[TaskOutput]) -> None:
404
394
  """
405
395
  Execute the task asynchronously with context handling.
406
396
  """
407
397
 
408
- result = self._execute_core(agent, context)
398
+ result = self._execute_core(agent, context, tools)
409
399
  future.set_result(result)
410
400
 
411
401
 
412
- def _execute_core(self, agent, context: Optional[str]) -> TaskOutput:
402
+ def _execute_core(self, agent, context: Optional[str], tools: Optional[List[Any]] = []) -> TaskOutput:
413
403
  """
414
404
  Run the core execution logic of the task.
415
405
  To speed up the process, when the format is not expected to return, we will skip the conversion process.
@@ -436,7 +426,7 @@ Your outputs MUST adhere to the following format and should NOT include any irre
436
426
  agent = agent_to_delegate
437
427
  self.delegations += 1
438
428
 
439
- output_raw, output_json_dict, output_pydantic = agent.execute_task(task=self, context=context), None, None
429
+ output_raw, output_json_dict, output_pydantic = agent.execute_task(task=self, context=context, tools=tools), None, None
440
430
 
441
431
  if self.expected_output_json:
442
432
  output_json_dict = self.create_json_output(raw_result=output_raw)
@@ -472,24 +462,32 @@ Your outputs MUST adhere to the following format and should NOT include any irre
472
462
  return task_output
473
463
 
474
464
 
465
+ def _store_execution_log(self, task_index: int, was_replayed: bool = False, inputs: Optional[Dict[str, Any]] = {}) -> None:
466
+ """
467
+ Store the task execution log.
468
+ """
469
+
470
+ self._task_output_handler.update(task=self, task_index=task_index, was_replayed=was_replayed, inputs=inputs)
471
+
472
+
473
+
475
474
  class ConditionalTask(Task):
476
475
  """
477
476
  A task that can be conditionally executed based on the output of another task.
478
- Use this with `Team`.
477
+ When the `condition` return True, execute the task, else skipped with `skipped task output`.
479
478
  """
480
479
 
481
480
  condition: Callable[[TaskOutput], bool] = Field(
482
481
  default=None,
483
- description="max. number of retries for an agent to execute a task when an error occurs.",
482
+ description="max. number of retries for an agent to execute a task when an error occurs",
484
483
  )
485
484
 
486
- def __init__(
487
- self,
488
- condition: Callable[[Any], bool],
489
- **kwargs,
490
- ):
485
+
486
+ def __init__(self, condition: Callable[[Any], bool], **kwargs):
491
487
  super().__init__(**kwargs)
492
488
  self.condition = condition
489
+ self._logger = Logger(verbose=True)
490
+
493
491
 
494
492
  def should_execute(self, context: TaskOutput) -> bool:
495
493
  """
@@ -498,5 +496,25 @@ class ConditionalTask(Task):
498
496
  """
499
497
  return self.condition(context)
500
498
 
499
+
501
500
  def get_skipped_task_output(self):
502
- return TaskOutput(task_id=self.id, raw="", pydantic=None, json_dict=None)
501
+ return TaskOutput(task_id=self.id, raw="", pydantic=None, json_dict={})
502
+
503
+
504
+ def _handle_conditional_task(self, task_outputs: List[TaskOutput], task_index: int, was_replayed: bool) -> Optional[TaskOutput]:
505
+ """
506
+ When the conditional task should be skipped, return `skipped_task_output` as task_output else return None
507
+ """
508
+
509
+ previous_output = task_outputs[task_index - 1] if task_outputs and len(task_outputs) > 1 else None
510
+
511
+ if previous_output and not self.should_execute(previous_output):
512
+ self._logger.log(level="debug", message=f"Skipping conditional task: {self.description}", color="yellow")
513
+ skipped_task_output = self.get_skipped_task_output()
514
+ self.output = skipped_task_output
515
+
516
+ if not was_replayed:
517
+ self._store_execution_log(self, task_index=task_index, was_replayed=was_replayed, inputs={})
518
+ return skipped_task_output
519
+
520
+ return None
versionhq/team/model.py CHANGED
@@ -6,7 +6,7 @@ from enum import Enum
6
6
  from dotenv import load_dotenv
7
7
  from concurrent.futures import Future
8
8
  from hashlib import md5
9
- from typing import Any, Dict, List, TYPE_CHECKING, Callable, Optional, Tuple, Union
9
+ from typing import Any, Dict, List, TYPE_CHECKING, Callable, Optional, Tuple
10
10
  from pydantic import UUID4, InstanceOf, Json, BaseModel, Field, PrivateAttr, field_validator, model_validator
11
11
  from pydantic._internal._generate_schema import GenerateSchema
12
12
  from pydantic_core import PydanticCustomError, core_schema
@@ -61,7 +61,7 @@ class TeamOutput(BaseModel):
61
61
  team_id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True, description="store the team ID that generate the TeamOutput")
62
62
  raw: str = Field(default="", description="raw output of the team lead task handled by the team leader")
63
63
  pydantic: Optional[Any] = Field(default=None, description="`raw` converted to the abs. pydantic model")
64
- json_dict: Union[Dict[str, Any]] = Field(default=None, description="`raw` converted to dictionary")
64
+ json_dict: Dict[str, Any] = Field(default=None, description="`raw` converted to dictionary")
65
65
  task_output_list: list[TaskOutput] = Field(default=list, description="store output of all the tasks that the team has executed")
66
66
  token_usage: UsageMetrics = Field(default=dict, description="processed token summary")
67
67
 
@@ -188,14 +188,9 @@ class Team(BaseModel):
188
188
  2. manager_task,
189
189
  3. members' tasks
190
190
  """
191
- sorted_member_tasks = [
192
- member.task for member in self.members if member.is_manager == True
193
- ] + [member.task for member in self.members if member.is_manager == False]
194
- return (
195
- self.team_tasks + sorted_member_tasks
196
- if len(self.team_tasks) > 0
197
- else sorted_member_tasks
198
- )
191
+ sorted_member_tasks = [member.task for member in self.members if member.is_manager == True] + [member.task for member in self.members if member.is_manager == False]
192
+ return self.team_tasks + sorted_member_tasks if len(self.team_tasks) > 0 else sorted_member_tasks
193
+
199
194
 
200
195
  # validators
201
196
  @field_validator("id", mode="before")
@@ -205,10 +200,6 @@ class Team(BaseModel):
205
200
  if v:
206
201
  raise PydanticCustomError("may_not_set_field", "The 'id' field cannot be set by the user.", {})
207
202
 
208
- # @field_validator("config", mode="before")
209
- # @classmethod
210
- # def check_config_type(cls, v: Union[Json, Dict[str, Any]]) -> Union[Json, Dict[str, Any]]:
211
- # return json.loads(v) if isinstance(v, Json) else v
212
203
 
213
204
  @model_validator(mode="after")
214
205
  def check_manager_llm(self):
@@ -261,7 +252,7 @@ class Team(BaseModel):
261
252
  if task.async_execution:
262
253
  async_task_count += 1
263
254
  else:
264
- break # stop traversing when a non-async task is found
255
+ break
265
256
 
266
257
  if async_task_count > 1:
267
258
  raise PydanticCustomError(
@@ -287,47 +278,23 @@ class Team(BaseModel):
287
278
  result[task_id] if hasattr(result, str(task_id)) else result
288
279
  )
289
280
 
281
+
290
282
  # task execution
291
283
  def _process_async_tasks(
292
- self, futures: List[Tuple[Task, Future[TaskOutput], int]], was_replayed: bool = False
293
- ) -> List[TaskOutput]:
284
+ self, futures: List[Tuple[Task, Future[TaskOutput], int]], was_replayed: bool = False
285
+ ) -> List[TaskOutput]:
286
+ """
287
+ When we have `Future` tasks, updated task outputs and task execution logs accordingly.
288
+ """
289
+
294
290
  task_outputs: List[TaskOutput] = []
291
+
295
292
  for future_task, future, task_index in futures:
296
293
  task_output = future.result()
297
294
  task_outputs.append(task_output)
298
- self._process_task_result(future_task, task_output)
299
- self._store_execution_log(
300
- future_task, task_output, task_index, was_replayed
301
- )
302
- return task_outputs
303
-
304
-
305
- def _handle_conditional_task(
306
- self,
307
- task: ConditionalTask,
308
- task_outputs: List[TaskOutput],
309
- futures: List[Tuple[Task, Future[TaskOutput], int]],
310
- task_index: int,
311
- was_replayed: bool,
312
- ) -> Optional[TaskOutput]:
313
-
314
- if futures:
315
- task_outputs = self._process_async_tasks(futures, was_replayed)
316
- futures.clear()
317
-
318
- previous_output = task_outputs[task_index - 1] if task_outputs else None
319
- if previous_output is not None and not task.should_execute(previous_output):
320
- self._logger.log(
321
- "debug",
322
- f"Skipping conditional task: {task.description}",
323
- color="yellow",
324
- )
325
- skipped_task_output = task.get_skipped_task_output()
295
+ future_task._store_execution_log(task_index, was_replayed)
326
296
 
327
- if not was_replayed:
328
- self._store_execution_log(task, skipped_task_output, task_index)
329
- return skipped_task_output
330
- return None
297
+ return task_outputs
331
298
 
332
299
 
333
300
  def _create_team_output(self, task_outputs: List[TaskOutput], lead_task_output: TaskOutput = None) -> TeamOutput:
@@ -401,38 +368,33 @@ class Team(BaseModel):
401
368
  if responsible_agent is None:
402
369
  responsible_agent = self.manager_agent if self.manager_agent else self.members[0].agent
403
370
 
404
- # self._prepare_agent_tools(task)
405
- # self._log_task_start(task, responsible_agent)
406
-
407
371
  if isinstance(task, ConditionalTask):
408
- skipped_task_output = self._handle_conditional_task(task, task_outputs, futures, task_index, was_replayed)
372
+ skipped_task_output = task._handle_conditional_task(task_outputs, futures, task_index, was_replayed)
409
373
  if skipped_task_output:
410
374
  continue
411
375
 
376
+ # self._prepare_agent_tools(task)
377
+ # self._log_task_start(task, responsible_agent)
378
+
412
379
  if task.async_execution:
413
380
  context = create_raw_outputs(tasks=[task, ],task_outputs=([last_sync_output,] if last_sync_output else []))
414
- future = task.execute_async(agent=responsible_agent, context=context
415
- # tools=responsible_agent.tools
416
- )
381
+ future = task.execute_async(agent=responsible_agent, context=context, tools=responsible_agent.tools)
417
382
  futures.append((task, future, task_index))
418
383
  else:
419
- if futures:
420
- task_outputs = self._process_async_tasks(futures, was_replayed)
421
- futures.clear()
422
-
423
- context = create_raw_outputs(tasks=[task,], task_outputs=([ last_sync_output,] if last_sync_output else [] ))
424
- task_output = task.execute_sync(agent=responsible_agent, context=context
425
- # tools=responsible_agent.tools
384
+ context = create_raw_outputs(tasks=[task,], task_outputs=([last_sync_output,] if last_sync_output else [] ))
385
+ task_output = task.execute_sync(agent=responsible_agent, context=context, tools=responsible_agent.tools
426
386
  )
427
387
  if responsible_agent is self.manager_agent:
428
388
  lead_task_output = task_output
429
389
 
430
390
  task_outputs.append(task_output)
431
391
  # self._process_task_result(task, task_output)
432
- # self._store_execution_log(task, task_output, task_index, was_replayed)
392
+ task._store_execution_log(task_index, was_replayed)
393
+
394
+
395
+ if futures:
396
+ task_outputs = self._process_async_tasks(futures, was_replayed)
433
397
 
434
- # if futures:
435
- # task_outputs = self._process_async_tasks(futures, was_replayed)
436
398
  return self._create_team_output(task_outputs, lead_task_output)
437
399
 
438
400
 
@@ -458,10 +420,6 @@ class Team(BaseModel):
458
420
  # self._task_output_handler.reset()
459
421
  # self._logging_color = "bold_purple"
460
422
 
461
- # if inputs is not None:
462
- # self._inputs = inputs
463
- # self._interpolate_inputs(inputs)
464
-
465
423
 
466
424
  # i18n = I18N(prompt_file=self.prompt_file)
467
425
 
@@ -469,14 +427,13 @@ class Team(BaseModel):
469
427
  agent = member.agent
470
428
  agent.team = self
471
429
 
472
- # add the team's common callbacks to each agent.
473
- if not agent.function_calling_llm:
430
+ if not agent.function_calling_llm and self.function_calling_llm:
474
431
  agent.function_calling_llm = self.function_calling_llm
475
432
 
476
433
  # if agent.allow_code_execution:
477
434
  # agent.tools += agent.get_code_execution_tools()
478
435
 
479
- if not agent.step_callback:
436
+ if not agent.step_callback and self.step_callback:
480
437
  agent.step_callback = self.step_callback
481
438
 
482
439
  if self.process is None:
versionhq/tool/model.py CHANGED
@@ -1,15 +1,7 @@
1
1
  from abc import ABC
2
2
  from inspect import signature
3
3
  from typing import Any, Dict, Callable, Type, Optional, get_args, get_origin
4
- from pydantic import (
5
- InstanceOf,
6
- BaseModel,
7
- ConfigDict,
8
- Field,
9
- create_model,
10
- field_validator,
11
- model_validator,
12
- )
4
+ from pydantic import InstanceOf, BaseModel, ConfigDict, Field, create_model, field_validator, model_validator
13
5
 
14
6
  from versionhq._utils.cache_handler import CacheHandler
15
7
 
@@ -135,6 +127,7 @@ class Tool(ABC, BaseModel):
135
127
 
136
128
  return cls(name=tool_name, func=func, args_schema=args_schema)
137
129
 
130
+
138
131
  def run(self, *args, **kwargs) -> Any:
139
132
  """
140
133
  Use the tool.
@@ -1,4 +1,5 @@
1
- from typing import Any, Optional, Union
1
+ from typing import Any, Optional
2
+ from pydantic import InstanceOf
2
3
  from versionhq.tool.model import ToolCalled, InstructorToolCalled, CacheTool
3
4
  from versionhq._utils.cache_handler import CacheHandler
4
5
 
@@ -23,7 +24,7 @@ class ToolHandler:
23
24
 
24
25
  def record_last_tool_used(
25
26
  self,
26
- last_used_tool: Union[ToolCalled, InstructorToolCalled],
27
+ last_used_tool: InstanceOf[ToolCalled] | InstanceOf[InstructorToolCalled],
27
28
  output: str,
28
29
  should_cache: bool = True,
29
30
  ) -> Any:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: versionhq
3
- Version: 1.1.7.5
3
+ Version: 1.1.7.8
4
4
  Summary: LLM orchestration frameworks for model-agnostic AI agents that handle complex outbound workflows
5
5
  Author-email: Kuriko Iwai <kuriko@versi0n.io>
6
6
  License: MIT License
@@ -32,6 +32,9 @@ Keywords: orchestration framework,orchestration,ai agent,multi-agent system,RAG,
32
32
  Classifier: Programming Language :: Python
33
33
  Classifier: License :: OSI Approved :: MIT License
34
34
  Classifier: Operating System :: OS Independent
35
+ Classifier: Development Status :: 3 - Alpha
36
+ Classifier: Intended Audience :: Developers
37
+ Classifier: Topic :: Software Development :: Build Tools
35
38
  Requires-Python: >=3.12
36
39
  Description-Content-Type: text/markdown
37
40
  License-File: LICENSE
@@ -43,12 +46,12 @@ Requires-Dist: typing
43
46
  Requires-Dist: json-repair>=0.31.0
44
47
  Requires-Dist: litellm>=1.55.8
45
48
  Requires-Dist: openai>=1.57.0
46
- Requires-Dist: composio-openai>=0.6.0
47
- Requires-Dist: pre-commit>=4.0.1
48
- Requires-Dist: gunicorn>=23.0.0
49
+ Requires-Dist: composio-openai>=0.6.9
49
50
  Requires-Dist: composio>=0.1.0
50
51
  Requires-Dist: setuptools>=75.6.0
51
52
  Requires-Dist: wheel>=0.45.1
53
+ Requires-Dist: python-dotenv>=1.0.0
54
+ Requires-Dist: appdirs>=1.4.4
52
55
 
53
56
  # Overview
54
57
 
@@ -56,7 +59,7 @@ Requires-Dist: wheel>=0.45.1
56
59
  [![Publisher](https://github.com/versionHQ/multi-agent-system/actions/workflows/publish.yml/badge.svg)](https://github.com/versionHQ/multi-agent-system/actions/workflows/publish.yml)
57
60
  ![PyPI](https://img.shields.io/badge/PyPI-v1.1.7.5-blue)
58
61
  ![python ver](https://img.shields.io/badge/Python-3.12/3.13-purple)
59
- ![pyenv ver](https://img.shields.io/badge/pyenv-2.4.23-orange)
62
+ ![pyenv ver](https://img.shields.io/badge/pyenv-2.5.0-orange)
60
63
 
61
64
 
62
65
  An LLM orchestration frameworks for multi-agent systems with RAG to autopilot outbound workflows.
@@ -276,8 +279,7 @@ src/
276
279
  ```
277
280
  uv venv
278
281
  source .venv/bin/activate
279
-
280
- uv pip install -r requirements.txt -v
282
+ uv pip sync
281
283
  ```
282
284
 
283
285
  * In case of AssertionError/module mismatch, run Python version control using `.pyenv`
@@ -1,14 +1,14 @@
1
- versionhq/__init__.py,sha256=HUTPmQJfxelI9dE_8G3mSK6--29L_B9zbIs_adJ1SJI,871
1
+ versionhq/__init__.py,sha256=KTM1HF1lTl5AiAuFQuGwltg4LEZjHnac9mz8h12MtGg,871
2
2
  versionhq/_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  versionhq/_utils/cache_handler.py,sha256=zDQKzIn7vp-M2-uepHFxgJstjfftZS5mzXKL_-4uVvI,370
4
4
  versionhq/_utils/i18n.py,sha256=TwA_PnYfDLA6VqlUDPuybdV9lgi3Frh_ASsb_X8jJo8,1483
5
5
  versionhq/_utils/logger.py,sha256=lqRYH45KHMQ4mwE1woa5xNmngYu4O749AYECsnWWpmA,1851
6
- versionhq/_utils/process_config.py,sha256=ogrhovLbwe0ocQlcohRgBBRtww7C3pk9hikjvgDzT5U,919
6
+ versionhq/_utils/process_config.py,sha256=UqoWD5IR4VLxEDGxIyVUylw_ppXwk8Wx1ynVuD-pUSg,822
7
7
  versionhq/_utils/rpm_controller.py,sha256=T7waIGeblu5K58erY4lqVLcPsWM7W9UFdU3DG9Dsk0w,2214
8
8
  versionhq/_utils/usage_metrics.py,sha256=c33a_28y8ECUgflsKN3mkNm0fNkWgZmXwybMwIqoKXA,1098
9
9
  versionhq/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- versionhq/agent/model.py,sha256=h9QHB-Hi_71JinPlfL_83kaB-Ks8npzIqE7BVHwn1Aw,18439
11
- versionhq/agent/parser.py,sha256=GhoNQo4WloVM3vGnAmt9lnEOTARX7nWMhJE55rF_5Rs,5500
10
+ versionhq/agent/model.py,sha256=tv14XkjrgsFsryFWzaw0w2h0X1T0ffipVK9l4kYbpIE,18598
11
+ versionhq/agent/parser.py,sha256=db5kfk-lR1Ph9-rsTvSeW1NjR6GJ00iaqTNYxJy3N8o,5487
12
12
  versionhq/agent/TEMPLATES/Backstory.py,sha256=cdngBx1GEv7nroR46FEhnysnBJ9mEVL763_9np6Skkc,395
13
13
  versionhq/agent/TEMPLATES/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  versionhq/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -18,22 +18,25 @@ versionhq/clients/customer/model.py,sha256=rQnCv_wdCdrYAsUjyB6X6ULiuWfqcBBoarXcQ
18
18
  versionhq/clients/product/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  versionhq/clients/product/model.py,sha256=Us3UnzYlub6ipBislMN-JrvxZx0ocl9PtQJINJ8XtBA,2385
20
20
  versionhq/clients/workflow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- versionhq/clients/workflow/model.py,sha256=qpRCDwULhSLWDFkSYrvXW5m07bKDrwttTmqsYA9ZVP4,5727
21
+ versionhq/clients/workflow/model.py,sha256=GI-cSw-7zOFzLC2Xa4YfCowu3MI0JyH33qDFTqbJLmg,5725
22
22
  versionhq/llm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  versionhq/llm/llm_vars.py,sha256=YZoXqFBW7XpclUZ14_AAz7WOjoyCXnGcI959GSpX2q0,5343
24
- versionhq/llm/model.py,sha256=PdwisrlrsDqd6gXwXCyGbGTRTeGZ8SXpt_gfua8qunk,8266
24
+ versionhq/llm/model.py,sha256=mXzSuf1s6MebGT7_yqgNppde0NIlAF8bjIXAp2MZ9Uw,8247
25
+ versionhq/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
+ versionhq/storage/task_output_storage.py,sha256=RxvF_lSRUuo2Af3zMAuc1aOymx1e6e_5VJ2y757_Hu0,4910
25
27
  versionhq/task/__init__.py,sha256=g4mCATnn1mUXxsfQ5p6IpPawr8O421wVIT8kMKEcxQw,180
26
28
  versionhq/task/formatter.py,sha256=N8Kmk9vtrMtBdgJ8J7RmlKNMdZWSmV8O1bDexmCWgU0,643
27
- versionhq/task/model.py,sha256=4Uh0OBLUO_YXaejxHbOHtNa4vckgI3abSaPz80_s9X4,18519
29
+ versionhq/task/log_handler.py,sha256=KJRrcNZgFSKhlNzvtYFnvtp6xukaF1s7ifX9u4zWrN8,1683
30
+ versionhq/task/model.py,sha256=bUmERE6AZfs8qh2Hb9LES5BLUlXrkrOUyWxCU27M1ic,19317
28
31
  versionhq/team/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
- versionhq/team/model.py,sha256=RyspmYVtXW3f4MKjWT1mnpM9XdE435d8HbEQms7LHMU,19821
32
+ versionhq/team/model.py,sha256=T_71FarXEzAxrTn_8yYVWMwLS9p-UxSovexZtHYnYn0,18066
30
33
  versionhq/team/team_planner.py,sha256=B1UOn_DYVVterUn2CAd80jfO4sViJCCXPJA3abSSugg,2143
31
34
  versionhq/tool/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
35
  versionhq/tool/decorator.py,sha256=Y-j4jkoujD5LUvpe8uf3p5Zagk2XVaRKC9rkIE-2geo,1189
33
- versionhq/tool/model.py,sha256=JZOEcZRIEfcrjL8DgrFYDt4YNgMF8rXS26RK6D2x9mc,6906
34
- versionhq/tool/tool_handler.py,sha256=e-2VfG9zFpfPG_oMoPXye93GDovs7FuUASWQwUTLrJ0,1498
35
- versionhq-1.1.7.5.dist-info/LICENSE,sha256=7CCXuMrAjPVsUvZrsBq9DsxI2rLDUSYXR_qj4yO_ZII,1077
36
- versionhq-1.1.7.5.dist-info/METADATA,sha256=g95oUiLVJPtABZEz1CLjq2opMbjWMLtD0XLXu3zG4Rw,15801
37
- versionhq-1.1.7.5.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
38
- versionhq-1.1.7.5.dist-info/top_level.txt,sha256=DClQwxDWqIUGeRJkA8vBlgeNsYZs4_nJWMonzFt5Wj0,10
39
- versionhq-1.1.7.5.dist-info/RECORD,,
36
+ versionhq/tool/model.py,sha256=s-y8323ikd5m5U2HG59ATgFW6L7yIFiPLLdOpeXQ8RI,6874
37
+ versionhq/tool/tool_handler.py,sha256=esUqGp8HoREesai8fmh2klAf04Sjpsacmb03C7F6sNQ,1541
38
+ versionhq-1.1.7.8.dist-info/LICENSE,sha256=7CCXuMrAjPVsUvZrsBq9DsxI2rLDUSYXR_qj4yO_ZII,1077
39
+ versionhq-1.1.7.8.dist-info/METADATA,sha256=xsI-HWU0_Rb0OLH_tSDvIk7E8SWP0U-SjTQfoomW6zU,15919
40
+ versionhq-1.1.7.8.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
41
+ versionhq-1.1.7.8.dist-info/top_level.txt,sha256=DClQwxDWqIUGeRJkA8vBlgeNsYZs4_nJWMonzFt5Wj0,10
42
+ versionhq-1.1.7.8.dist-info/RECORD,,