versionhq 1.1.9.4__tar.gz → 1.1.9.9__tar.gz

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.
Files changed (79) hide show
  1. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/.github/workflows/publish.yml +1 -1
  2. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/.github/workflows/publish_testpypi.yml +3 -3
  3. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/.github/workflows/run_tests.yml +1 -1
  4. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/PKG-INFO +1 -1
  5. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/pyproject.toml +1 -1
  6. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/__init__.py +1 -1
  7. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/agent/model.py +13 -22
  8. versionhq-1.1.9.9/src/versionhq/clients/customer/__init__.py +10 -0
  9. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/clients/customer/model.py +29 -13
  10. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/clients/product/model.py +40 -3
  11. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/clients/workflow/model.py +10 -4
  12. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/task/model.py +19 -36
  13. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/tool/model.py +0 -34
  14. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq.egg-info/PKG-INFO +1 -1
  15. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq.egg-info/SOURCES.txt +1 -0
  16. versionhq-1.1.9.9/tests/clients/customer_test.py +31 -0
  17. versionhq-1.1.9.9/tests/clients/product_test.py +26 -0
  18. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/tests/clients/workflow_test.py +3 -1
  19. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/tests/task/task_test.py +30 -10
  20. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/uv.lock +10 -10
  21. versionhq-1.1.9.4/src/versionhq/clients/customer/__init__.py +0 -5
  22. versionhq-1.1.9.4/tests/clients/product_test.py +0 -11
  23. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/.github/workflows/security_check.yml +0 -0
  24. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/.gitignore +0 -0
  25. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/.pre-commit-config.yaml +0 -0
  26. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/.python-version +0 -0
  27. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/LICENSE +0 -0
  28. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/README.md +0 -0
  29. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/SECURITY.md +0 -0
  30. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/db/preprocess.py +0 -0
  31. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/requirements-dev.txt +0 -0
  32. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/requirements.txt +0 -0
  33. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/runtime.txt +0 -0
  34. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/setup.cfg +0 -0
  35. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/_utils/__init__.py +0 -0
  36. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/_utils/cache_handler.py +0 -0
  37. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/_utils/i18n.py +0 -0
  38. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/_utils/logger.py +0 -0
  39. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/_utils/process_config.py +0 -0
  40. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/_utils/rpm_controller.py +0 -0
  41. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/_utils/usage_metrics.py +0 -0
  42. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/agent/TEMPLATES/Backstory.py +0 -0
  43. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/agent/TEMPLATES/__init__.py +0 -0
  44. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/agent/__init__.py +0 -0
  45. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/agent/parser.py +0 -0
  46. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/cli/__init__.py +0 -0
  47. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/clients/__init__.py +0 -0
  48. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/clients/product/__init__.py +0 -0
  49. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/clients/workflow/__init__.py +0 -0
  50. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/llm/__init__.py +0 -0
  51. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/llm/llm_vars.py +0 -0
  52. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/llm/model.py +0 -0
  53. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/storage/__init__.py +0 -0
  54. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/storage/task_output_storage.py +0 -0
  55. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/task/__init__.py +0 -0
  56. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/task/formatter.py +0 -0
  57. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/task/log_handler.py +0 -0
  58. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/team/__init__.py +0 -0
  59. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/team/model.py +0 -0
  60. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/team/team_planner.py +0 -0
  61. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/tool/__init__.py +0 -0
  62. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/tool/composio_tool.py +0 -0
  63. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/tool/decorator.py +0 -0
  64. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq/tool/tool_handler.py +0 -0
  65. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq.egg-info/dependency_links.txt +0 -0
  66. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq.egg-info/requires.txt +0 -0
  67. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/src/versionhq.egg-info/top_level.txt +0 -0
  68. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/tests/__init__.py +0 -0
  69. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/tests/agent/__init__.py +0 -0
  70. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/tests/agent/agent_test.py +0 -0
  71. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/tests/cli/__init__.py +0 -0
  72. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/tests/conftest.py +0 -0
  73. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/tests/task/__init__.py +0 -0
  74. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/tests/team/Prompts/Demo_test.py +0 -0
  75. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/tests/team/__init__.py +0 -0
  76. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/tests/team/team_test.py +0 -0
  77. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/tests/tool/__init__.py +0 -0
  78. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/tests/tool/composio_test.py +0 -0
  79. {versionhq-1.1.9.4 → versionhq-1.1.9.9}/tests/tool/tool_test.py +0 -0
@@ -21,7 +21,7 @@ jobs:
21
21
 
22
22
  - name: Build release distributions
23
23
  run: |
24
- uv venv --python=python3.13.1
24
+ uv venv --python 3.13.1
25
25
  source .venv/bin/activate
26
26
  uv pip install --upgrade pip twine
27
27
  uv pip install -r requirements.txt
@@ -19,10 +19,10 @@ jobs:
19
19
  uses: astral-sh/setup-uv@v4
20
20
  with:
21
21
  version: "0.5.11"
22
-
22
+
23
23
  - name: Build release distributions
24
24
  run: |
25
- uv venv --python=python3.13.1
25
+ uv venv --python 3.13.1
26
26
  source .venv/bin/activate
27
27
  uv pip install --upgrade pip twine
28
28
  uv pip install -r requirements.txt
@@ -38,7 +38,7 @@ jobs:
38
38
  runs-on: ubuntu-latest
39
39
  needs:
40
40
  - release-build
41
-
41
+
42
42
  permissions:
43
43
  id-token: write
44
44
 
@@ -28,7 +28,7 @@ jobs:
28
28
 
29
29
  - name: Set up the project
30
30
  run: |
31
- uv venv --python=python3.13.1
31
+ uv venv --python 3.13.1
32
32
  source .venv/bin/activate
33
33
  uv pip install --upgrade pip pytest
34
34
  uv pip install -r requirements.txt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: versionhq
3
- Version: 1.1.9.4
3
+ Version: 1.1.9.9
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
@@ -15,7 +15,7 @@ exclude = ["test*", "__pycache__"]
15
15
 
16
16
  [project]
17
17
  name = "versionhq"
18
- version = "1.1.9.4"
18
+ version = "1.1.9.9"
19
19
  authors = [{ name = "Kuriko Iwai", email = "kuriko@versi0n.io" }]
20
20
  description = "LLM orchestration frameworks for model-agnostic AI agents that handle complex outbound workflows"
21
21
  readme = "README.md"
@@ -18,7 +18,7 @@ from versionhq.tool.model import Tool
18
18
  from versionhq.tool.composio_tool import ComposioHandler
19
19
 
20
20
 
21
- __version__ = "1.1.9.4"
21
+ __version__ = "1.1.9.9"
22
22
  __all__ = [
23
23
  "Agent",
24
24
  "Customer",
@@ -322,7 +322,7 @@ class Agent(BaseModel):
322
322
  When encountering errors, we try the task execution up to `self.max_retry_limit` times.
323
323
  """
324
324
 
325
- task_execution_counter = 0
325
+ task_execution_counter, raw_response = 0, None
326
326
 
327
327
  messages = []
328
328
  messages.append({"role": "user", "content": prompts}) #! REFINEME
@@ -331,31 +331,28 @@ class Agent(BaseModel):
331
331
 
332
332
  callbacks = kwargs.get("callbacks", None)
333
333
 
334
- response = self.llm.call(
335
- messages=messages,
336
- output_formats=output_formats,
337
- field_list=response_fields,
338
- callbacks=callbacks,
334
+ raw_response = self.llm.call(
335
+ messages=messages, output_formats=output_formats, field_list=response_fields, callbacks=callbacks
339
336
  )
340
337
  task_execution_counter += 1
341
- self._logger.log(level="info", message=f"Agent's first response: {response}", color="blue")
338
+ self._logger.log(level="info", message=f"Agent's first response in {type(raw_response).__name__}: {raw_response}", color="blue")
342
339
 
343
- if (response is None or response == "") and task_execution_counter < self.max_retry_limit:
340
+ if (raw_response is None or raw_response == "") and task_execution_counter < self.max_retry_limit:
344
341
  while task_execution_counter <= self.max_retry_limit:
345
- response = self.llm.call(
342
+ raw_response = self.llm.call(
346
343
  messages=messages,
347
344
  output_formats=output_formats,
348
345
  field_list=response_fields,
349
346
  callbacks=callbacks,
350
347
  )
351
348
  task_execution_counter += 1
352
- self._logger.log(level="info", message=f"Agent's next response: {response}", color="blue")
349
+ self._logger.log(level="info", message=f"Agent's next response in {type(raw_response).__name__}: {raw_response}", color="blue")
353
350
 
354
- elif response is None or response == "":
351
+ elif raw_response is None or raw_response == "":
355
352
  self._logger.log(level="error", message="Received None or empty response from the model", color="red")
356
353
  raise ValueError("Invalid response from LLM call - None or empty.")
357
354
 
358
- return {"output": response.output if hasattr(response, "output") else response}
355
+ return raw_response
359
356
 
360
357
 
361
358
  def execute_task(self, task, context: Optional[str] = None) -> str:
@@ -392,31 +389,25 @@ class Agent(BaseModel):
392
389
  if task.take_tool_res_as_final:
393
390
  return tool_results
394
391
 
395
-
396
-
397
392
  # if self.team and self.team._train:
398
393
  # task_prompt = self._training_handler(task_prompt=task_prompt)
399
394
  # else:
400
395
  # task_prompt = self._use_trained_data(task_prompt=task_prompt)
401
396
 
402
397
  try:
403
- result = self.invoke(
398
+ raw_response = self.invoke(
404
399
  prompts=task_prompt,
405
400
  output_formats=task.expected_output_formats,
406
401
  response_fields=task.output_field_list,
407
- )["output"]
402
+ )
408
403
 
409
404
  except Exception as e:
410
405
  self._times_executed += 1
411
406
  if self._times_executed > self.max_retry_limit:
412
407
  raise e
413
- result = self.execute_task(task, context)
408
+ raw_response = self.execute_task(task, context)
414
409
 
415
410
  if self.max_rpm and self._rpm_controller:
416
411
  self._rpm_controller.stop_rpm_counter()
417
412
 
418
- # for tool_result in self.tools_results:
419
- # if tool_result.get("result_as_answer", False):
420
- # result = tool_result["result"]
421
-
422
- return result
413
+ return raw_response
@@ -0,0 +1,10 @@
1
+ from enum import Enum
2
+
3
+
4
+ class Status(str, Enum):
5
+ NOT_ASSIGNED = 0
6
+ READY_TO_DEPLOY = 1
7
+ ACTIVE_ON_WORKFLOW = 2
8
+ INACTIVE_ON_WORKFLOW = 3 # inactive customer
9
+ EXIT_WITH_CONVERSION = 4
10
+ SUSPENDED = 5
@@ -10,14 +10,32 @@ from versionhq.clients.customer import Status
10
10
 
11
11
  class BaseCustomer(ABC, BaseModel):
12
12
  """
13
- Abstract base class for the base customer
13
+ Abstract base class for the base customer storing current status on the workflow and deployment method.
14
+ """
15
+ status: Status = Field(default=Status.NOT_ASSIGNED)
16
+
17
+ @abstractmethod
18
+ def _deploy(self, *args, **kwargs) -> Any:
19
+ """Any method to deploy targeting the customer"""
20
+
21
+
22
+ def deploy(self, *args, **kwargs) -> Any:
23
+ if self.status is Status.READY_TO_DEPLOY:
24
+ return self._deploy(self, **args, **kwargs)
25
+
26
+
27
+
28
+ class Customer(BaseCustomer):
29
+ """
30
+ Customer class to store customer info and handle deployment methods.
14
31
  """
15
32
 
16
33
  id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
17
34
  name: Optional[str] = Field(default=None, description="customer's name if any")
18
35
  products: Optional[List[Product]] = Field(default=list, description="store products that the customer is associated with")
19
36
  analysis: str = Field(default=None, description="store the latest analysis results on the customer")
20
- status: str = Field(default=Status.ON_WORKFLOW)
37
+ function: Optional[Callable] = Field(default=None, descripition="store deploy function")
38
+ config: Optional[Dict[str, Any]] = Field(default=None, description="config to the function")
21
39
 
22
40
 
23
41
  @field_validator("id", mode="before")
@@ -27,11 +45,10 @@ class BaseCustomer(ABC, BaseModel):
27
45
  raise PydanticCustomError("may_not_set_field", "This field is not to be set by the user.", {})
28
46
 
29
47
 
30
- def customer_to(self) -> List[ProductProvider]:
48
+ def fetch_product_providers(self) -> List[ProductProvider] | None:
31
49
  """
32
50
  Return list of ProductProvider if the customer has `product_list`
33
51
  """
34
-
35
52
  res = []
36
53
  if self.products:
37
54
  for item in self.products:
@@ -39,15 +56,14 @@ class BaseCustomer(ABC, BaseModel):
39
56
  res.appned(item.provider)
40
57
  return res
41
58
 
59
+ def _deploy(self, *args, **kwargs):
60
+ return self.deploy(self, *args, **kwargs)
42
61
 
43
- @abstractmethod
44
- def _deploy(self, *args, **kwargs) -> Any:
45
- """Any method to deploy targeting the customer"""
46
62
 
63
+ def deploy(self, *args, **kwargs):
64
+ self.status = Status.ACTIVE_ON_WORKFLOW
47
65
 
48
- class Customer(BaseCustomer):
49
- id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
50
- name: Optional[str] = Field(default=None, description="customer's name if any")
51
- products: Optional[List[Product]] = Field(default=list, description="store products that the customer is associated with")
52
- analysis: str = Field(default=None, description="store the latest analysis results on the customer")
53
- status: str = Field(default=Status.ON_WORKFLOW)
66
+ if self.function:
67
+ return self.function(**self.config)
68
+
69
+ return super().deploy(*args, **kwargs)
@@ -16,7 +16,7 @@ class ProductProvider(ABC, BaseModel):
16
16
  id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
17
17
  name: Optional[str] = Field(default=None)
18
18
  region: Optional[str] = Field(default=None, description="region of client's main business operation")
19
- data_pipelines: Optional[List[ComposioAppName | str]] = Field(default_factory=list)
19
+ data_pipelines: Optional[List[ComposioAppName | str]] = Field(default=None)
20
20
  destination_services: Optional[List[ComposioAppName | str]] = Field(default=None)
21
21
 
22
22
  @field_validator("id", mode="before")
@@ -26,12 +26,41 @@ class ProductProvider(ABC, BaseModel):
26
26
  raise PydanticCustomError("may_not_set_field", "This field is not to be set by the user.", {})
27
27
 
28
28
 
29
+ @model_validator(mode="after")
30
+ def set_up_destinations(self):
31
+ """
32
+ Set up the destination services and data pipeines using ComposioAppName class.
33
+ """
34
+ if self.destination_services is not None:
35
+ results = []
36
+ for item in self.destination_services:
37
+ if isinstance(item, ComposioAppName):
38
+ results.append(item)
39
+ elif isinstance(item, str) and item in ComposioAppName:
40
+ results.append(ComposioAppName(item))
41
+ else:
42
+ results.append(item)
43
+ self.destination_services = results
44
+
45
+ if self.data_pipelines is not None:
46
+ results = []
47
+ for item in self.data_pipelines:
48
+ if isinstance(item, ComposioAppName):
49
+ results.append(item)
50
+ elif isinstance(item, str) and item in ComposioAppName:
51
+ results.append(ComposioAppName(item))
52
+ else:
53
+ results.append(item)
54
+ self.data_pipelines = results
55
+
56
+ return self
57
+
58
+
29
59
 
30
60
  class Product(BaseModel):
31
61
  """
32
- Store the product information necessary to the outbound effrots and connect it to the `ProductProvider` instance.
62
+ A class to store product information used to create outbound
33
63
  """
34
-
35
64
  id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
36
65
  name: Optional[str] = Field(default=None, description="product name")
37
66
  description: Optional[str] = Field(
@@ -44,6 +73,7 @@ class Product(BaseModel):
44
73
  usp: Optional[str] = Field(default=None)
45
74
  landing_url: Optional[str] = Field(default=None, description="marketing url of the product if any")
46
75
  cohort_timeframe: Optional[int] = Field(default=30, description="ideal cohort timeframe of the product in days")
76
+ notes: Optional[str] = Field(default=None, description="any notes from the client to consider. cascade to the agent")
47
77
 
48
78
 
49
79
  @field_validator("id", mode="before")
@@ -51,3 +81,10 @@ class Product(BaseModel):
51
81
  def _deny_user_set_id(cls, v: Optional[UUID4]) -> None:
52
82
  if v:
53
83
  raise PydanticCustomError("may_not_set_field", "This field is not to be set by the user.", {})
84
+
85
+
86
+ @field_validator("cohort_timeframe", mode="before")
87
+ @classmethod
88
+ def _deny_non_int_input(cls, v: Optional[UUID4]) -> None:
89
+ if not isinstance(v, int):
90
+ raise PydanticCustomError("invalid_input", "This field only accepts inputs in integer.", {})
@@ -121,15 +121,21 @@ class MessagingWorkflow(ABC, BaseModel):
121
121
  @model_validator(mode="after")
122
122
  def set_up_destination(self):
123
123
  """
124
- Set up the destination service when self.destination is None.
125
- Prioritize customer's destination to the product provider's destination list.
124
+ Set up the destination service using ComposioAppName class.
126
125
  """
127
- if self.destination is None:
126
+ if isinstance(self.destination, ComposioAppName):
127
+ pass
128
+
129
+ elif isinstance(self.destination, str) and self.destination in ComposioAppName:
130
+ self.destination = ComposioAppName(self.destination)
131
+
132
+ elif self.destination is None:
128
133
  # if self.customer is not None:
129
134
  # self.destination = self.customer.on
130
135
 
131
136
  if self.product.provider is not None and self.product.provider.destination_services:
132
- self.destination = self.product.provider.destination_services[0]
137
+ applied_service = self.product.provider.destination_services[0]
138
+ self.destination = ComposioAppName(applied_service) if applied_service in ComposioAppName else applied_service
133
139
 
134
140
  return self
135
141
 
@@ -78,20 +78,11 @@ class TaskOutput(BaseModel):
78
78
  return str(self.pydantic) if self.pydantic else str(self.json_dict) if self.json_dict else self.raw
79
79
 
80
80
 
81
- def to_dict(self) -> Dict[str, Any]:
81
+ def to_dict(self) -> Dict[str, Any] | None:
82
82
  """
83
83
  Convert pydantic / raw output into dict and return the dict.
84
- When we only have `raw` output, return `{ output: raw }` to avoid an error
85
84
  """
86
-
87
- output_dict = {}
88
- if self.json_dict:
89
- output_dict.update(self.json_dict)
90
- elif self.pydantic:
91
- output_dict.update(self.pydantic.model_dump())
92
- else:
93
- output_dict.upate({ "output": self.raw })
94
- return output_dict
85
+ return self.json_dict if self.json_dict is not None else self.pydantic.model_dump() if self.pydantic else None
95
86
 
96
87
 
97
88
  def context_prompting(self) -> str:
@@ -245,36 +236,28 @@ class Task(BaseModel):
245
236
  return "\n".join(task_slices)
246
237
 
247
238
 
248
- def create_json_output(self, raw_result: Any) -> Optional[Dict[str, Any]]:
239
+ def _create_json_output(self, raw_result: str) -> Dict[str, Any]:
249
240
  """
250
241
  Create json (dict) output from the raw result.
251
242
  """
243
+ import ast
252
244
 
253
- output_json_dict: Optional[Dict[str, Any]] = None
245
+ output_json_dict: Dict[str, Any] = dict()
254
246
 
255
- if isinstance(raw_result, BaseModel):
256
- output_json_dict = raw_result.model_dump()
247
+ try:
248
+ r = json.dumps(eval(str(raw_result)))
249
+ output_json_dict = json.loads(r)
257
250
 
258
- elif isinstance(raw_result, dict):
259
- output_json_dict = raw_result
251
+ if isinstance(output_json_dict, str):
252
+ output_json_dict = ast.literal_eval(r)
260
253
 
261
- elif isinstance(raw_result, str):
262
- try:
263
- output_json_dict = json.loads(raw_result)
264
- except json.JSONDecodeError:
265
- try:
266
- output_json_dict = eval(raw_result)
267
- except:
268
- try:
269
- import ast
270
- output_json_dict = ast.literal_eval(raw_result)
271
- except:
272
- output_json_dict = { "output": raw_result }
254
+ except:
255
+ output_json_dict = { "output": raw_result }
273
256
 
274
257
  return output_json_dict
275
258
 
276
259
 
277
- def create_pydantic_output(self, output_json_dict: Dict[str, Any], raw_result: Any = None) -> Optional[Any]:
260
+ def _create_pydantic_output(self, output_json_dict: Dict[str, Any], raw_result: Any = None) -> Optional[Any]:
278
261
  """
279
262
  Create pydantic output from the `raw` result.
280
263
  """
@@ -379,18 +362,18 @@ class Task(BaseModel):
379
362
  agent = agent_to_delegate
380
363
  self.delegations += 1
381
364
 
382
- if self.take_tool_res_as_final is True:
365
+ if self.take_tool_res_as_final == True:
383
366
  output = agent.execute_task(task=self, context=context)
384
367
  task_output = TaskOutput(task_id=self.id, tool_output=output)
385
368
 
386
369
  else:
387
370
  output_raw, output_json_dict, output_pydantic = agent.execute_task(task=self, context=context), None, None
388
371
 
389
- if self.expected_output_json:
390
- output_json_dict = self.create_json_output(raw_result=output_raw)
372
+ if self.expected_output_json == True:
373
+ output_json_dict = self._create_json_output(raw_result=output_raw)
391
374
 
392
- if self.expected_output_pydantic:
393
- output_pydantic = self.create_pydantic_output(output_json_dict=output_json_dict)
375
+ if self.expected_output_pydantic == True:
376
+ output_pydantic = self._create_pydantic_output(output_json_dict=output_json_dict)
394
377
 
395
378
  task_output = TaskOutput(
396
379
  task_id=self.id,
@@ -441,7 +424,7 @@ class Task(BaseModel):
441
424
  output_formats_to_follow[item.title] = f"<Return your answer in {item.type.__name__}>"
442
425
 
443
426
  output_prompt = f"""
444
- Your outputs MUST adhere to the following format and should NOT include any irrelevant elements:
427
+ Output only valid JSON conforming to the specified format. Use double quotes for keys and values:
445
428
  {output_formats_to_follow}
446
429
  """
447
430
  return output_prompt
@@ -174,40 +174,6 @@ class Tool(BaseTool):
174
174
  return f"Tool Name: {self.name}\nTool Arguments: {args_schema}\nGoal: {self.goal}"
175
175
 
176
176
 
177
-
178
- # @classmethod
179
- # def from_composio(
180
- # cls, func: Callable = None, tool_name: str = "Composio tool"
181
- # ) -> "Tool":
182
- # """
183
- # Create Tool from the composio tool, ensuring the Tool instance has a func to be executed.
184
- # Refer to the `args_schema` from the func signature if any. Else, create an `args_schema`.
185
- # """
186
-
187
- # if not func:
188
- # raise ValueError("Params must have a callable 'function' attribute.")
189
-
190
- # # args_schema = getattr(tool, "args_schema", None)
191
- # args_fields = {}
192
- # func_signature = signature(func)
193
- # annotations = func_signature.parameters
194
- # for name, param in annotations.items():
195
- # if name != "self":
196
- # param_annotation = (
197
- # param.annotation if param.annotation != param.empty else Any
198
- # )
199
- # field_info = Field(default=..., description="")
200
- # args_fields[name] = (param_annotation, field_info)
201
-
202
- # args_schema = (
203
- # create_model(f"{tool_name}Input", **args_fields)
204
- # if args_fields
205
- # else create_model(f"{tool_name}Input", __base__=BaseModel)
206
- # )
207
-
208
- # return cls(name=tool_name, func=func, args_schema=args_schema)
209
-
210
-
211
177
  class ToolSet(BaseModel):
212
178
  """
213
179
  Store the tool called and any kwargs used.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: versionhq
3
- Version: 1.1.9.4
3
+ Version: 1.1.9.9
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
@@ -62,6 +62,7 @@ tests/conftest.py
62
62
  tests/agent/__init__.py
63
63
  tests/agent/agent_test.py
64
64
  tests/cli/__init__.py
65
+ tests/clients/customer_test.py
65
66
  tests/clients/product_test.py
66
67
  tests/clients/workflow_test.py
67
68
  tests/task/__init__.py
@@ -0,0 +1,31 @@
1
+ import os
2
+ import pytest
3
+ import datetime
4
+ from typing import List, Dict, Any, Optional, Callable
5
+
6
+ from versionhq.clients.product.model import Product, ProductProvider
7
+ from versionhq.clients.customer.model import Customer, Status
8
+ from versionhq.tool import ComposioAppName
9
+
10
+
11
+ def test_create_customer():
12
+ provider = ProductProvider(name="demo", region="US", data_pipelines=["hubspot", "demo crm"], destination_services=["test"])
13
+ product = Product(name="demo", description="demo", provider=provider, landing_url="www.com", cohort_timeframe=30)
14
+
15
+ def custom_deploy(item):
16
+ return f"custom deploy with {item}"
17
+
18
+ class CustomCustomer(Customer):
19
+ name: str = "custom"
20
+ staus: Status = Status.READY_TO_DEPLOY
21
+ products: List[Product] = [product,]
22
+ analysis: str = "analysis"
23
+ function: Optional[Callable] = custom_deploy
24
+ config: Optional[Dict[str, Any]] = dict(item="custom config")
25
+
26
+ customer = CustomCustomer()
27
+
28
+ assert customer.id is not None
29
+ assert customer.deploy() == "custom deploy with custom config"
30
+ assert customer.status == Status.ACTIVE_ON_WORKFLOW
31
+ assert [isinstance(item, Product) for item in customer.products]
@@ -0,0 +1,26 @@
1
+ import os
2
+ import pytest
3
+ import datetime
4
+
5
+ from versionhq.clients.workflow.model import Score, ScoreFormat, MessagingWorkflow, MessagingComponent
6
+ from versionhq.clients.product.model import Product, ProductProvider
7
+ from versionhq.tool import ComposioAppName
8
+
9
+
10
+ def test_create_product_provider():
11
+ provider = ProductProvider(name="demo", region="US", data_pipelines=["hubspot", "demo crm"] )
12
+
13
+ assert provider.id is not None
14
+ assert provider.name == "demo"
15
+ assert isinstance(provider.data_pipelines[0], ComposioAppName)
16
+ assert isinstance(provider.data_pipelines[1], str)
17
+ assert provider.destination_services is None
18
+
19
+
20
+ def test_create_product():
21
+ provider = ProductProvider(name="demo", region="US", data_pipelines=["hubspot", "demo crm"], destination_services=["test"])
22
+ product = Product(name="demo", description="demo", provider=provider, audience="demo", usp="demo", landing_url="www.com", cohort_timeframe=30)
23
+
24
+ assert product.id is not None
25
+ assert product.name == "demo"
26
+ assert product.provider.id is not None
@@ -4,6 +4,7 @@ import datetime
4
4
 
5
5
  from versionhq.clients.workflow.model import Score, ScoreFormat, MessagingWorkflow, MessagingComponent
6
6
  from versionhq.clients.product.model import Product, ProductProvider
7
+ from versionhq.tool import ComposioAppName
7
8
 
8
9
 
9
10
  def test_store_scores():
@@ -44,7 +45,7 @@ def test_setup_messaging_workflow_with_provider():
44
45
  name="demo provider",
45
46
  region="US",
46
47
  data_pipelines=["data"],
47
- destination_services=["email", "linkedin",]
48
+ destination_services=["linkedin", "email",]
48
49
  )
49
50
  product = Product(
50
51
  description="demo p",
@@ -60,3 +61,4 @@ def test_setup_messaging_workflow_with_provider():
60
61
  assert messaging_workflow.id is not None
61
62
  assert messaging_workflow.destination is not None
62
63
  assert messaging_workflow.destination in provider.destination_services
64
+ assert isinstance(messaging_workflow.destination, ComposioAppName)
@@ -52,19 +52,13 @@ def test_sync_execute_task():
52
52
 
53
53
 
54
54
  def test_async_execute_task():
55
- agent = Agent(role="demo agent 2", goal="My amazing goals")
56
- task = Task(
57
- description="Analyze the client's business model and define the optimal cohort timeframe.",
58
- output_field_list=[
59
- ResponseField(title="test1", type=str, required=True),
60
- ResponseField(title="test2", type=list, required=True),
61
- ],
62
- )
55
+ agent = Agent(role="demo agent", goal="My amazing goals")
56
+ task = Task(description="Return string: 'test'")
63
57
 
64
- with patch.object(Agent, "execute_task", return_value="ok") as execute:
58
+ with patch.object(Agent, "execute_task", return_value="test") as execute:
65
59
  execution = task.execute_async(agent=agent)
66
60
  result = execution.result()
67
- assert result.raw == "ok"
61
+ assert result.raw == "test"
68
62
  execute.assert_called_once_with(task=task, context=None)
69
63
 
70
64
 
@@ -301,4 +295,30 @@ def test_task_with_tools():
301
295
  assert res.tool_output[2] == "empty function"
302
296
 
303
297
 
298
+ def test_create_json_dict_output():
299
+ agent = Agent(role="demo agent 6", goal="My amazing goals")
300
+ task = Task(
301
+ description="Analyze the client's business model.",
302
+ output_field_list=[
303
+ ResponseField(title="str", type=str, required=True),
304
+ ResponseField(title="list", type=list),
305
+ ResponseField(title="dict", type=dict)
306
+ ],
307
+ )
308
+
309
+ res = task.execute_sync(agent=agent)
310
+
311
+ assert isinstance(res, TaskOutput)
312
+ assert isinstance(res.json_dict, dict)
313
+ assert [k == "str" for k, v in res.json_dict.items()]
314
+ assert [k == "list" for k, v in res.json_dict.items()]
315
+ assert [k == "dict" for k, v in res.json_dict.items()]
316
+ assert isinstance(res.json_dict["str"], str)
317
+ assert isinstance(res.json_dict["list"], list)
318
+ assert isinstance(res.json_dict["dict"], dict)
319
+
320
+
304
321
  # token usage logic
322
+
323
+ if __name__ == "__main__":
324
+ test_create_json_dict_output()
@@ -853,14 +853,14 @@ wheels = [
853
853
 
854
854
  [[package]]
855
855
  name = "langchain-text-splitters"
856
- version = "0.3.4"
856
+ version = "0.3.5"
857
857
  source = { registry = "https://pypi.org/simple" }
858
858
  dependencies = [
859
859
  { name = "langchain-core" },
860
860
  ]
861
- sdist = { url = "https://files.pythonhosted.org/packages/10/a5/215816cc376c4f3853c961ae98022a6d57e74f6859c206f29716173e9a73/langchain_text_splitters-0.3.4.tar.gz", hash = "sha256:f3cedea469684483b4492d9f11dc2fa66388dab01c5d5c5307925515ab884c24", size = 22205 }
861
+ sdist = { url = "https://files.pythonhosted.org/packages/10/35/a6f8d6b1bb0e6e8c00b49bce4d1a115f8b68368b1899f65bb34dbbb44160/langchain_text_splitters-0.3.5.tar.gz", hash = "sha256:11cb7ca3694e5bdd342bc16d3875b7f7381651d4a53cbb91d34f22412ae16443", size = 26318 }
862
862
  wheels = [
863
- { url = "https://files.pythonhosted.org/packages/a2/37/b2c971dfea62675f640e0dd5a078ca502885059276d21b7143cf1fe13e82/langchain_text_splitters-0.3.4-py3-none-any.whl", hash = "sha256:432fdb39c161d4d0db16d61d38af068dc5dd4dd08082febd2fced81304b2725c", size = 27783 },
863
+ { url = "https://files.pythonhosted.org/packages/4b/83/f8081c3bea416bd9d9f0c26af795c74f42c24f9ad3c4fbf361b7d69de134/langchain_text_splitters-0.3.5-py3-none-any.whl", hash = "sha256:8c9b059827438c5fa8f327b4df857e307828a5ec815163c9b5c9569a3e82c8ee", size = 31620 },
864
864
  ]
865
865
 
866
866
  [[package]]
@@ -896,7 +896,7 @@ wheels = [
896
896
 
897
897
  [[package]]
898
898
  name = "litellm"
899
- version = "1.57.1"
899
+ version = "1.57.2"
900
900
  source = { registry = "https://pypi.org/simple" }
901
901
  dependencies = [
902
902
  { name = "aiohttp" },
@@ -911,9 +911,9 @@ dependencies = [
911
911
  { name = "tiktoken" },
912
912
  { name = "tokenizers" },
913
913
  ]
914
- sdist = { url = "https://files.pythonhosted.org/packages/5c/4f/0319ff9bd29ed0c900158fa0afe4ccf4365de2d1b6b572c6212ffbee835e/litellm-1.57.1.tar.gz", hash = "sha256:2ce6ce1707c92fb278f828a8ea058fa12b3eeb8081dd8c10776569995e03bb6f", size = 6299132 }
914
+ sdist = { url = "https://files.pythonhosted.org/packages/d4/1c/e5669149f8f1d57103e97c28a8f06fc9e111370d4b6d0b4dfc76f4a11fe4/litellm-1.57.2.tar.gz", hash = "sha256:0a07c4e288f4bd9033335d5606d7da497f1193d51cf262b96812f40b8842a842", size = 6299758 }
915
915
  wheels = [
916
- { url = "https://files.pythonhosted.org/packages/09/02/2760c82c7f7779933527d348ea6272f41f6ff8bb29b9e301cc66ff7b5754/litellm-1.57.1-py3-none-any.whl", hash = "sha256:f9e93689f2d96df3bcebe723d44b6e2e71b9b047ec7ebd1054b6c9bc96cd9515", size = 6582106 },
916
+ { url = "https://files.pythonhosted.org/packages/9a/46/d96a6902c3529f96f8ea1fc2ba08fd48b50652141ddce9bf8faf0acdbbe6/litellm-1.57.2-py3-none-any.whl", hash = "sha256:b572c0d3d3c33ff3a4d18928ac6f051d10ac159814017a817d88ec7af9a8600c", size = 6582805 },
917
917
  ]
918
918
 
919
919
  [[package]]
@@ -1137,7 +1137,7 @@ wheels = [
1137
1137
 
1138
1138
  [[package]]
1139
1139
  name = "openai"
1140
- version = "1.59.3"
1140
+ version = "1.59.4"
1141
1141
  source = { registry = "https://pypi.org/simple" }
1142
1142
  dependencies = [
1143
1143
  { name = "anyio" },
@@ -1149,9 +1149,9 @@ dependencies = [
1149
1149
  { name = "tqdm" },
1150
1150
  { name = "typing-extensions" },
1151
1151
  ]
1152
- sdist = { url = "https://files.pythonhosted.org/packages/73/d0/def3c7620e1cb446947f098aeac9d88fc826b1760d66da279e4712d37666/openai-1.59.3.tar.gz", hash = "sha256:7f7fff9d8729968588edf1524e73266e8593bb6cab09298340efb755755bb66f", size = 344192 }
1152
+ sdist = { url = "https://files.pythonhosted.org/packages/38/db/0e1376bdee3de8c16d91647d47dc47a26d2d6036931c76844e7d3e3fb989/openai-1.59.4.tar.gz", hash = "sha256:b946dc5a2308dc1e03efbda80bf1cd64b6053b536851ad519f57ee44401663d2", size = 344405 }
1153
1153
  wheels = [
1154
- { url = "https://files.pythonhosted.org/packages/c7/26/0e0fb582bcb2a7cb6802447a749a2fc938fe4b82324097abccb86abfd5d1/openai-1.59.3-py3-none-any.whl", hash = "sha256:b041887a0d8f3e70d1fc6ffbb2bf7661c3b9a2f3e806c04bf42f572b9ac7bc37", size = 454793 },
1154
+ { url = "https://files.pythonhosted.org/packages/99/01/1eefc235bb79174826b2fa0cad05bc2eab90eae97bf78c765887d7430e46/openai-1.59.4-py3-none-any.whl", hash = "sha256:82113498699998e98104f87c19a890e82df9b01251a0395484360575d3a1d98a", size = 454810 },
1155
1155
  ]
1156
1156
 
1157
1157
  [[package]]
@@ -1964,7 +1964,7 @@ wheels = [
1964
1964
 
1965
1965
  [[package]]
1966
1966
  name = "versionhq"
1967
- version = "1.1.9.3"
1967
+ version = "1.1.9.9"
1968
1968
  source = { editable = "." }
1969
1969
  dependencies = [
1970
1970
  { name = "appdirs" },
@@ -1,5 +0,0 @@
1
- from enum import Enum
2
-
3
-
4
- class Status(str, Enum):
5
- ON_WORKFLOW = "on_workflow"
@@ -1,11 +0,0 @@
1
- import os
2
- import pytest
3
- import datetime
4
-
5
- from versionhq.clients.workflow.model import Score, ScoreFormat, MessagingWorkflow, MessagingComponent
6
- from versionhq.clients.product.model import Product, ProductProvider
7
-
8
-
9
- def test_create_product():
10
- provider = ProductProvider()
11
- product = Product()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes