versionhq 1.1.8.1__tar.gz → 1.1.9.0__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 (77) hide show
  1. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/.github/workflows/run_tests.yml +3 -0
  2. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/.gitignore +1 -1
  3. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/PKG-INFO +2 -2
  4. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/README.md +1 -1
  5. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/pyproject.toml +1 -1
  6. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/__init__.py +3 -1
  7. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/agent/model.py +1 -1
  8. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/clients/workflow/model.py +8 -9
  9. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/task/model.py +72 -71
  10. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/team/model.py +51 -57
  11. versionhq-1.1.9.0/src/versionhq/tool/__init__.py +53 -0
  12. versionhq-1.1.9.0/src/versionhq/tool/composio.py +149 -0
  13. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/tool/decorator.py +3 -1
  14. versionhq-1.1.9.0/src/versionhq/tool/model.py +228 -0
  15. versionhq-1.1.9.0/src/versionhq/tool/tool_handler.py +43 -0
  16. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq.egg-info/PKG-INFO +2 -2
  17. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq.egg-info/SOURCES.txt +5 -1
  18. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/tests/task/task_test.py +6 -35
  19. versionhq-1.1.9.0/tests/tool/composio_test.py +19 -0
  20. versionhq-1.1.9.0/tests/tool/tool_test.py +83 -0
  21. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/uv.lock +64 -64
  22. versionhq-1.1.8.1/src/versionhq/tool/model.py +0 -202
  23. versionhq-1.1.8.1/src/versionhq/tool/tool_handler.py +0 -48
  24. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/.github/workflows/publish.yml +0 -0
  25. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/.github/workflows/publish_testpypi.yml +0 -0
  26. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/.github/workflows/security_check.yml +0 -0
  27. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/.pre-commit-config.yaml +0 -0
  28. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/.python-version +0 -0
  29. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/LICENSE +0 -0
  30. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/SECURITY.md +0 -0
  31. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/db/preprocess.py +0 -0
  32. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/requirements-dev.txt +0 -0
  33. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/requirements.txt +0 -0
  34. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/runtime.txt +0 -0
  35. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/setup.cfg +0 -0
  36. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/_utils/__init__.py +0 -0
  37. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/_utils/cache_handler.py +0 -0
  38. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/_utils/i18n.py +0 -0
  39. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/_utils/logger.py +0 -0
  40. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/_utils/process_config.py +0 -0
  41. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/_utils/rpm_controller.py +0 -0
  42. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/_utils/usage_metrics.py +0 -0
  43. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/agent/TEMPLATES/Backstory.py +0 -0
  44. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/agent/TEMPLATES/__init__.py +0 -0
  45. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/agent/__init__.py +0 -0
  46. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/agent/parser.py +0 -0
  47. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/cli/__init__.py +0 -0
  48. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/clients/__init__.py +0 -0
  49. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/clients/customer/__init__.py +0 -0
  50. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/clients/customer/model.py +0 -0
  51. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/clients/product/__init__.py +0 -0
  52. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/clients/product/model.py +0 -0
  53. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/clients/workflow/__init__.py +0 -0
  54. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/llm/__init__.py +0 -0
  55. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/llm/llm_vars.py +0 -0
  56. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/llm/model.py +0 -0
  57. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/storage/__init__.py +0 -0
  58. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/storage/task_output_storage.py +0 -0
  59. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/task/__init__.py +0 -0
  60. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/task/formatter.py +0 -0
  61. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/task/log_handler.py +0 -0
  62. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/team/__init__.py +0 -0
  63. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq/team/team_planner.py +0 -0
  64. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq.egg-info/dependency_links.txt +0 -0
  65. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq.egg-info/requires.txt +0 -0
  66. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/src/versionhq.egg-info/top_level.txt +0 -0
  67. {versionhq-1.1.8.1/src/versionhq/tool → versionhq-1.1.9.0/tests}/__init__.py +0 -0
  68. {versionhq-1.1.8.1/tests → versionhq-1.1.9.0/tests/agent}/__init__.py +0 -0
  69. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/tests/agent/agent_test.py +0 -0
  70. {versionhq-1.1.8.1/tests/agent → versionhq-1.1.9.0/tests/cli}/__init__.py +0 -0
  71. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/tests/clients/workflow_test.py +0 -0
  72. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/tests/conftest.py +0 -0
  73. {versionhq-1.1.8.1/tests/cli → versionhq-1.1.9.0/tests/task}/__init__.py +0 -0
  74. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/tests/team/Prompts/Demo_test.py +0 -0
  75. {versionhq-1.1.8.1/tests/task → versionhq-1.1.9.0/tests/team}/__init__.py +0 -0
  76. {versionhq-1.1.8.1 → versionhq-1.1.9.0}/tests/team/team_test.py +0 -0
  77. {versionhq-1.1.8.1/tests/team → versionhq-1.1.9.0/tests/tool}/__init__.py +0 -0
@@ -8,6 +8,9 @@ permissions:
8
8
  env:
9
9
  LITELLM_API_KEY: ${{ secrets.LITELLM_API_KEY }}
10
10
  OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
11
+ DEFAULT_REDIRECT_URL: ${{ secrets.DEFAULT_REDIRECT_URL }}
12
+ DEFAULT_USER_ID: ${{ secrets.DEFAULT_USER_ID }}
13
+ COMPOSIO_API_KEY: ${{ secrets.COMPOSIO_API_KEY }}
11
14
 
12
15
  jobs:
13
16
  run_test:
@@ -1,7 +1,7 @@
1
1
  knowledge/
2
2
  memory/
3
3
 
4
- composio.py
4
+ # composio.py
5
5
  memo.txt
6
6
 
7
7
  dist/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: versionhq
3
- Version: 1.1.8.1
3
+ Version: 1.1.9.0
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
@@ -312,7 +312,7 @@ src/
312
312
  4. Test the features using the `tests` directory.
313
313
 
314
314
  - Add a test function to respective components in the `tests` directory.
315
- - Add your `LITELLM_API_KEY` and `OPENAI_API_KEY` to the Github `repository secrets` @ settings > secrets & variables > Actions.
315
+ - Add your `LITELLM_API_KEY`, `OPENAI_API_KEY`, `COMPOSIO_API_KEY`, `DEFAULT_USER_ID` to the Github `repository secrets` @ settings > secrets & variables > Actions.
316
316
  - Run a test.
317
317
  ```
318
318
  uv run pytest tests -vv
@@ -257,7 +257,7 @@ src/
257
257
  4. Test the features using the `tests` directory.
258
258
 
259
259
  - Add a test function to respective components in the `tests` directory.
260
- - Add your `LITELLM_API_KEY` and `OPENAI_API_KEY` to the Github `repository secrets` @ settings > secrets & variables > Actions.
260
+ - Add your `LITELLM_API_KEY`, `OPENAI_API_KEY`, `COMPOSIO_API_KEY`, `DEFAULT_USER_ID` to the Github `repository secrets` @ settings > secrets & variables > Actions.
261
261
  - Run a test.
262
262
  ```
263
263
  uv run pytest tests -vv
@@ -15,7 +15,7 @@ exclude = ["test*", "__pycache__"]
15
15
 
16
16
  [project]
17
17
  name = "versionhq"
18
- version = "1.1.8.1"
18
+ version = "1.1.9.0"
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"
@@ -15,9 +15,10 @@ from versionhq.llm.model import LLM
15
15
  from versionhq.task.model import Task, TaskOutput
16
16
  from versionhq.team.model import Team, TeamOutput
17
17
  from versionhq.tool.model import Tool
18
+ from versionhq.tool.composio import Composio
18
19
 
19
20
 
20
- __version__ = "1.1.8.1"
21
+ __version__ = "1.1.9.0"
21
22
  __all__ = [
22
23
  "Agent",
23
24
  "Customer",
@@ -33,4 +34,5 @@ __all__ = [
33
34
  "Team",
34
35
  "TeamOutput",
35
36
  "Tool",
37
+ "Composio"
36
38
  ]
@@ -15,7 +15,7 @@ from versionhq.llm.llm_vars import LLM_VARS
15
15
  from versionhq.llm.model import LLM, DEFAULT_CONTEXT_WINDOW
16
16
  from versionhq.task import TaskOutputFormat
17
17
  from versionhq.task.model import ResponseField
18
- from versionhq.tool.model import Tool, ToolCalled
18
+ from versionhq.tool.model import Tool, ToolSet
19
19
  from versionhq.tool.tool_handler import ToolHandler
20
20
 
21
21
  load_dotenv(override=True)
@@ -117,15 +117,6 @@ class MessagingWorkflow(ABC, BaseModel):
117
117
  description="store metrics that used to predict and track the performance of this workflow."
118
118
  )
119
119
 
120
-
121
- @property
122
- def name(self):
123
- if self.customer.id:
124
- return f"Workflow ID: {self.id} - on {self.product.id} for {self.customer.id}"
125
- else:
126
- return f"Workflow ID: {self.id} - on {self.product.id}"
127
-
128
-
129
120
  @field_validator("id", mode="before")
130
121
  @classmethod
131
122
  def _deny_user_set_id(cls, v: Optional[UUID4]) -> None:
@@ -160,3 +151,11 @@ class MessagingWorkflow(ABC, BaseModel):
160
151
  self.agents = agents
161
152
  self.team = team
162
153
  self.updated_at = datetime.datetime.now()
154
+
155
+
156
+ @property
157
+ def name(self):
158
+ if self.customer.id:
159
+ return f"Workflow ID: {self.id} - on {self.product.id} for {self.customer.id}"
160
+ else:
161
+ return f"Workflow ID: {self.id} - on {self.product.id}"
@@ -12,7 +12,7 @@ from pydantic_core import PydanticCustomError
12
12
  from versionhq._utils.process_config import process_config
13
13
  from versionhq.task import TaskOutputFormat
14
14
  from versionhq.task.log_handler import TaskOutputStorageHandler
15
- from versionhq.tool.model import Tool, ToolCalled
15
+ from versionhq.tool.model import Tool, ToolSet
16
16
  from versionhq._utils.logger import Logger
17
17
 
18
18
 
@@ -76,18 +76,6 @@ class TaskOutput(BaseModel):
76
76
  def __str__(self) -> str:
77
77
  return str(self.pydantic) if self.pydantic else str(self.json_dict) if self.json_dict else self.raw
78
78
 
79
- @property
80
- def json(self) -> Optional[str]:
81
- if self.output_format != TaskOutputFormat.JSON:
82
- raise ValueError(
83
- """
84
- Invalid output format requested.
85
- If you would like to access the JSON output,
86
- pleae make sure to set the output_json property for the task
87
- """
88
- )
89
- return json.dumps(self.json_dict)
90
-
91
79
 
92
80
  def to_dict(self) -> Dict[str, Any]:
93
81
  """
@@ -112,6 +100,19 @@ class TaskOutput(BaseModel):
112
100
  return json.dumps(self.json_dict) if self.json_dict else self.raw[0: 127]
113
101
 
114
102
 
103
+ @property
104
+ def json(self) -> Optional[str]:
105
+ if self.output_format != TaskOutputFormat.JSON:
106
+ raise ValueError(
107
+ """
108
+ Invalid output format requested.
109
+ If you would like to access the JSON output,
110
+ pleae make sure to set the output_json property for the task
111
+ """
112
+ )
113
+ return json.dumps(self.json_dict)
114
+
115
+
115
116
 
116
117
  class Task(BaseModel):
117
118
  """
@@ -140,7 +141,7 @@ class Task(BaseModel):
140
141
 
141
142
  # task setup
142
143
  context: Optional[List["Task"]] = Field(default=None, description="other tasks whose outputs should be used as context")
143
- tools_called: Optional[List[ToolCalled]] = Field(default_factory=list, description="tools that the agent can use for this task")
144
+ tools_called: Optional[List[ToolSet]] = Field(default_factory=list, description="tools that the agent can use for this task")
144
145
  take_tool_res_as_final: bool = Field(default=False, description="when set True, tools res will be stored in the `TaskOutput`")
145
146
  allow_delegation: bool = Field(default=False, description="ask other agents for help and run the task instead")
146
147
 
@@ -157,63 +158,6 @@ class Task(BaseModel):
157
158
  delegations: int = 0
158
159
 
159
160
 
160
- @property
161
- def output_prompt(self) -> str:
162
- """
163
- Draft prompts on the output format by converting `output_field_list` to dictionary.
164
- """
165
-
166
- output_prompt, output_formats_to_follow = "", dict()
167
- for item in self.output_field_list:
168
- output_formats_to_follow[item.title] = f"<Return your answer in {item.type.__name__}>"
169
-
170
- output_prompt = f"""
171
- Your outputs MUST adhere to the following format and should NOT include any irrelevant elements:
172
- {output_formats_to_follow}
173
- """
174
- return output_prompt
175
-
176
-
177
- @property
178
- def expected_output_formats(self) -> List[TaskOutputFormat]:
179
- """
180
- Return output formats in list with the ENUM item.
181
- `TaskOutputFormat.RAW` is set as default.
182
- """
183
- outputs = [TaskOutputFormat.RAW,]
184
- if self.expected_output_json:
185
- outputs.append(TaskOutputFormat.JSON)
186
- if self.expected_output_pydantic:
187
- outputs.append(TaskOutputFormat.PYDANTIC)
188
- return outputs
189
-
190
-
191
- @property
192
- def key(self) -> str:
193
- output_format = (
194
- TaskOutputFormat.JSON
195
- if self.expected_output_json == True
196
- else (
197
- TaskOutputFormat.PYDANTIC
198
- if self.expected_output_pydantic == True
199
- else TaskOutputFormat.RAW
200
- )
201
- )
202
- source = [self.description, output_format]
203
- return md5("|".join(source).encode(), usedforsecurity=False).hexdigest()
204
-
205
-
206
- @property
207
- def summary(self) -> str:
208
- return f"""
209
- Task ID: {str(self.id)}
210
- "Description": {self.description}
211
- "Prompt": {self.output_prompt}
212
- "Tools": {", ".join([tool_called.tool.name for tool_called in self.tools_called])}
213
- """
214
-
215
-
216
- # validators
217
161
  @model_validator(mode="before")
218
162
  @classmethod
219
163
  def process_model_config(cls, values: Dict[str, Any]):
@@ -467,6 +411,63 @@ Your outputs MUST adhere to the following format and should NOT include any irre
467
411
  self._task_output_handler.update(task=self, task_index=task_index, was_replayed=was_replayed, inputs=inputs)
468
412
 
469
413
 
414
+ @property
415
+ def output_prompt(self) -> str:
416
+ """
417
+ Draft prompts on the output format by converting `output_field_list` to dictionary.
418
+ """
419
+
420
+ output_prompt, output_formats_to_follow = "", dict()
421
+ for item in self.output_field_list:
422
+ output_formats_to_follow[item.title] = f"<Return your answer in {item.type.__name__}>"
423
+
424
+ output_prompt = f"""
425
+ Your outputs MUST adhere to the following format and should NOT include any irrelevant elements:
426
+ {output_formats_to_follow}
427
+ """
428
+ return output_prompt
429
+
430
+
431
+ @property
432
+ def expected_output_formats(self) -> List[TaskOutputFormat]:
433
+ """
434
+ Return output formats in list with the ENUM item.
435
+ `TaskOutputFormat.RAW` is set as default.
436
+ """
437
+ outputs = [TaskOutputFormat.RAW,]
438
+ if self.expected_output_json:
439
+ outputs.append(TaskOutputFormat.JSON)
440
+ if self.expected_output_pydantic:
441
+ outputs.append(TaskOutputFormat.PYDANTIC)
442
+ return outputs
443
+
444
+
445
+ @property
446
+ def key(self) -> str:
447
+ output_format = (
448
+ TaskOutputFormat.JSON
449
+ if self.expected_output_json == True
450
+ else (
451
+ TaskOutputFormat.PYDANTIC
452
+ if self.expected_output_pydantic == True
453
+ else TaskOutputFormat.RAW
454
+ )
455
+ )
456
+ source = [self.description, output_format]
457
+ return md5("|".join(source).encode(), usedforsecurity=False).hexdigest()
458
+
459
+
460
+ @property
461
+ def summary(self) -> str:
462
+ return f"""
463
+ Task ID: {str(self.id)}
464
+ "Description": {self.description}
465
+ "Prompt": {self.output_prompt}
466
+ "Tools": {", ".join([tool_called.tool.name for tool_called in self.tools_called])}
467
+ """
468
+
469
+
470
+
470
471
 
471
472
  class ConditionalTask(Task):
472
473
  """
@@ -161,54 +161,6 @@ class Team(BaseModel):
161
161
  return self.name if self.name is not None else self.id.__str__
162
162
 
163
163
 
164
- @property
165
- def key(self) -> str:
166
- source = [str(member.agent.id.__str__) for member in self.members] + [str(task.id.__str__) for task in self.tasks]
167
- return md5("|".join(source).encode(), usedforsecurity=False).hexdigest()
168
-
169
-
170
- @property
171
- def managers(self) -> List[TeamMember] | None:
172
- managers = [member for member in self.members if member.is_manager == True]
173
- return managers if len(managers) > 0 else None
174
-
175
-
176
- @property
177
- def manager_tasks(self) -> List[Task] | None:
178
- """
179
- Tasks (incl. team tasks) handled by managers in the team.
180
- """
181
- if self.managers:
182
- tasks = [manager.task for manager in self.managers if manager.task is not None]
183
- return tasks if len(tasks) > 0 else None
184
-
185
- return None
186
-
187
-
188
- @property
189
- def tasks(self):
190
- """
191
- Return all the tasks that the team needs to handle in order of priority:
192
- 1. team tasks, -> assigned to the member
193
- 2. manager_task,
194
- 3. members' tasks
195
- """
196
-
197
- team_tasks = self.team_tasks
198
- manager_tasks = [member.task for member in self.members if member.is_manager == True and member.task is not None and member.task not in team_tasks]
199
- member_tasks = [member.task for member in self.members if member.is_manager == False and member.task is not None and member.task not in team_tasks]
200
-
201
- return team_tasks + manager_tasks + member_tasks
202
-
203
-
204
- @property
205
- def member_tasks_without_agent(self) -> List[Task] | None:
206
- if self.members:
207
- return [member.task for member in self.members if member.agent is None]
208
-
209
- return None
210
-
211
-
212
164
  @field_validator("id", mode="before")
213
165
  @classmethod
214
166
  def _deny_user_set_id(cls, v: Optional[UUID4]) -> None:
@@ -218,7 +170,7 @@ class Team(BaseModel):
218
170
 
219
171
 
220
172
  @model_validator(mode="after")
221
- def validate_tasks(self):
173
+ def assess_tasks(self):
222
174
  """
223
175
  Validates if the model recognize all tasks that the team needs to handle.
224
176
  """
@@ -261,7 +213,7 @@ class Team(BaseModel):
261
213
  if self.process == TaskHandlingProcess.sequential and self.team_tasks is None:
262
214
  for member in self.members:
263
215
  if member.task is None:
264
- raise PydanticCustomError("missing_agent_in_task", f"Sequential process error: Agent is missing the task", {})
216
+ raise PydanticCustomError("missing_agent_in_task", "Sequential process error: Agent is missing the task", {})
265
217
  return self
266
218
 
267
219
  @model_validator(mode="after")
@@ -280,11 +232,7 @@ class Team(BaseModel):
280
232
  break
281
233
 
282
234
  if async_task_count > 1:
283
- raise PydanticCustomError(
284
- "async_task_count",
285
- "The team must end with max. one asynchronous task.",
286
- {},
287
- )
235
+ raise PydanticCustomError("async_task_count", "The team must end with max. one asynchronous task.", {})
288
236
  return self
289
237
 
290
238
 
@@ -310,7 +258,6 @@ class Team(BaseModel):
310
258
  unassigned_tasks: List[Task] = self.member_tasks_without_agent
311
259
  new_team_members: List[TeamMember] = []
312
260
 
313
-
314
261
  if self.team_tasks:
315
262
  candidates = idling_managers + idling_members
316
263
  if candidates:
@@ -468,7 +415,6 @@ class Team(BaseModel):
468
415
 
469
416
  metrics: List[UsageMetrics] = []
470
417
 
471
-
472
418
  if self.team_tasks or self.member_tasks_without_agent:
473
419
  self._handle_team_planning()
474
420
 
@@ -511,3 +457,51 @@ class Team(BaseModel):
511
457
  self.usage_metrics.add_usage_metrics(metric)
512
458
 
513
459
  return result
460
+
461
+
462
+ @property
463
+ def key(self) -> str:
464
+ source = [str(member.agent.id.__str__) for member in self.members] + [str(task.id.__str__) for task in self.tasks]
465
+ return md5("|".join(source).encode(), usedforsecurity=False).hexdigest()
466
+
467
+
468
+ @property
469
+ def managers(self) -> List[TeamMember] | None:
470
+ managers = [member for member in self.members if member.is_manager == True]
471
+ return managers if len(managers) > 0 else None
472
+
473
+
474
+ @property
475
+ def manager_tasks(self) -> List[Task] | None:
476
+ """
477
+ Tasks (incl. team tasks) handled by managers in the team.
478
+ """
479
+ if self.managers:
480
+ tasks = [manager.task for manager in self.managers if manager.task is not None]
481
+ return tasks if len(tasks) > 0 else None
482
+
483
+ return None
484
+
485
+
486
+ @property
487
+ def tasks(self):
488
+ """
489
+ Return all the tasks that the team needs to handle in order of priority:
490
+ 1. team tasks, -> assigned to the member
491
+ 2. manager_task,
492
+ 3. members' tasks
493
+ """
494
+
495
+ team_tasks = self.team_tasks
496
+ manager_tasks = [member.task for member in self.members if member.is_manager == True and member.task is not None and member.task not in team_tasks]
497
+ member_tasks = [member.task for member in self.members if member.is_manager == False and member.task is not None and member.task not in team_tasks]
498
+
499
+ return team_tasks + manager_tasks + member_tasks
500
+
501
+
502
+ @property
503
+ def member_tasks_without_agent(self) -> List[Task] | None:
504
+ if self.members:
505
+ return [member.task for member in self.members if member.agent is None]
506
+
507
+ return None
@@ -0,0 +1,53 @@
1
+ from enum import Enum
2
+
3
+ DEFAULT_AUTH_SCHEME = "OAUTH2"
4
+
5
+ class ComposioAuthScheme(str, Enum):
6
+ OAUTH2 = "OAUTH2"
7
+ BEARER_TOKEN = "BEARER_TOKEN"
8
+ API_KEY = "API_KEY"
9
+
10
+
11
+ class ComposioAppName(str, Enum):
12
+ """
13
+ Enum to store app names that we can connect via Composio as data pipelines or destination services.
14
+ """
15
+
16
+ SALESFORCE = "salesforce"
17
+ AIRTABLE = "airtable"
18
+ MAILCHIMP = "mailchimp"
19
+ HUBSPOT = "hubspot"
20
+ KLAVIYO = "klaviyo"
21
+ GOOGLESHEET = "googlesheets"
22
+ GMAIL = "gmail"
23
+ FACEBOOK = "facebook"
24
+ TWITTER = "twitter"
25
+ TWITTER_MEDIA = "twitter_media"
26
+ LINKEDIN = "linkedin"
27
+
28
+
29
+ composio_app_set = [
30
+ (ComposioAppName.SALESFORCE, ComposioAuthScheme.OAUTH2),
31
+ (ComposioAppName.AIRTABLE, ComposioAuthScheme.OAUTH2, ComposioAuthScheme.API_KEY, ComposioAuthScheme.BEARER_TOKEN),
32
+ (ComposioAppName.MAILCHIMP, ComposioAuthScheme.OAUTH2),
33
+ (ComposioAppName.HUBSPOT, ComposioAuthScheme.OAUTH2, ComposioAuthScheme.BEARER_TOKEN),
34
+ (ComposioAppName.KLAVIYO, ComposioAuthScheme.OAUTH2, ComposioAuthScheme.API_KEY),
35
+ (ComposioAppName.GOOGLESHEET, ComposioAuthScheme.OAUTH2),
36
+ (ComposioAppName.GMAIL, ComposioAuthScheme.OAUTH2, ComposioAuthScheme.BEARER_TOKEN),
37
+ (ComposioAppName.TWITTER, ComposioAuthScheme.OAUTH2),
38
+ (ComposioAppName.TWITTER_MEDIA, ComposioAuthScheme.OAUTH2),
39
+ (ComposioAppName.FACEBOOK, ComposioAuthScheme.OAUTH2),
40
+ (ComposioAppName.LINKEDIN, ComposioAuthScheme.OAUTH2),
41
+ ]
42
+
43
+ class COMPOSIO_STATUS(str, Enum):
44
+ INITIATED = "initiated"
45
+ ACTIVE = "active"
46
+ FAILED = "failed"
47
+
48
+
49
+
50
+ class ComposioAction(str, Enum):
51
+ """
52
+ Enum to store composio's action that can be called via `Actions.xxx`
53
+ """
@@ -0,0 +1,149 @@
1
+ import os
2
+ import uuid
3
+ from dotenv import load_dotenv
4
+ from typing import Any, Callable, Type, get_args, get_origin, Optional, Tuple
5
+
6
+ from pydantic import BaseModel, Field, model_validator, field_validator, UUID4
7
+ from pydantic_core import PydanticCustomError
8
+
9
+ from composio import ComposioToolSet, Action, App, action
10
+
11
+ from versionhq.tool import ComposioAppName, ComposioAuthScheme, composio_app_set, COMPOSIO_STATUS
12
+
13
+ load_dotenv(override=True)
14
+
15
+ DEFAULT_REDIRECT_URL = os.environ.get("DEFAULT_REDIRECT_URL", None)
16
+ DEFAULT_USER_ID = os.environ.get("DEFAULT_USER_ID", None)
17
+
18
+
19
+ class Composio(BaseModel):
20
+ """
21
+ Class to handle composio tools.
22
+ """
23
+
24
+ id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
25
+ app_name: str = Field(default=ComposioAppName.HUBSPOT)
26
+ user_id: str = Field(default=DEFAULT_USER_ID)
27
+ auth_scheme: str = Field(default=ComposioAuthScheme.OAUTH2)
28
+ redirect_url: str = Field(default=DEFAULT_REDIRECT_URL, description="redirect url after successful oauth2 connection")
29
+ connect_request_id: str = Field(default=None, description="store the client's composio id to connect with the app")
30
+
31
+ @property
32
+ def toolset(self) -> ComposioToolSet:
33
+ return ComposioToolSet(api_key=os.environ.get("COMPOSIO_API_KEY"))
34
+
35
+
36
+ def __name__(self):
37
+ return self.app_name
38
+
39
+
40
+ @field_validator("id", mode="before")
41
+ @classmethod
42
+ def _deny_user_set_id(cls, v: Optional[UUID4]) -> None:
43
+ if v:
44
+ raise PydanticCustomError("may_not_set_field", "This field is not to be set by the user.", {})
45
+
46
+
47
+ # @model_validator("user_id", mode="before")
48
+ # @classmethod
49
+ # def _deny_no_user_id(cls, v: Optional[str]) -> None:
50
+ # if v is None:
51
+ # raise PydanticCustomError("user_id_missing", "Need user_id to connect with the tool", {})
52
+
53
+
54
+ @model_validator(mode="after")
55
+ def validate_app_name(self):
56
+ if self.app_name not in ComposioAppName:
57
+ raise PydanticCustomError("no_app_name", f"The app name {self.app_name} is not valid.", {})
58
+
59
+ return self
60
+
61
+
62
+ @model_validator(mode="after")
63
+ def validate_auth_scheme(self):
64
+ """
65
+ Raise error when the client uses auth scheme unavailable for the app.
66
+ """
67
+ app_set = next(filter(lambda tup: self.app_name in tup, composio_app_set), None)
68
+ if not app_set:
69
+ raise PydanticCustomError("no_app_set", f"The app set of {self.app_name} is missing.", {})
70
+
71
+ else:
72
+ acceptable_auth_scheme = next(filter(lambda item: self.auth_scheme in item, app_set), None)
73
+ if acceptable_auth_scheme is None:
74
+ raise PydanticCustomError("invalid_auth_scheme", f"The app {self.app_name} must have different auth_scheme.", {})
75
+
76
+ return self
77
+
78
+
79
+ # connect with composio to use the tool
80
+ def connect(self, token: Optional[str] = None, api_key: Optional[str] = None) -> Tuple[str | Any]:
81
+ """
82
+ Connect with Composio, retrieve `connect_request_id`, and validate the connection.
83
+ """
84
+
85
+ if not self.user_id:
86
+ raise PydanticCustomError("user_id_missing", "Need user_id to connect with the tool", {})
87
+
88
+
89
+ connection_request, connected_account = None, None
90
+
91
+ if self.auth_scheme == ComposioAuthScheme.API_KEY:
92
+ collected_from_user = {}
93
+ collected_from_user["api_key"] = api_key
94
+ connection_request = self.toolset.initiate_connection(
95
+ connected_account_params = collected_from_user,
96
+ app=self.app_name,
97
+ entity_id=self.user_id,
98
+ auth_scheme=self.auth_scheme,
99
+ )
100
+
101
+ if self.auth_scheme == ComposioAuthScheme.BEARER_TOKEN:
102
+ collected_from_user = {}
103
+ collected_from_user["token"] = token
104
+ connection_request = self.toolset.initiate_connection(
105
+ connected_account_params = collected_from_user,
106
+ app=self.app_name,
107
+ entity_id=self.user_id,
108
+ auth_scheme=self.auth_scheme,
109
+ )
110
+
111
+ if self.auth_scheme == ComposioAuthScheme.OAUTH2:
112
+ connection_request = self.toolset.initiate_connection(
113
+ app=self.app_name,
114
+ redirect_url = self.redirect_url, # clients will be redirected to this url after successful auth.
115
+ entity_id=self.user_id,
116
+ auth_scheme=self.auth_scheme,
117
+ )
118
+
119
+ # connection_request.wait_until_active(self.toolset.client, timeout=60)
120
+
121
+ if connection_request.connectionStatus is not COMPOSIO_STATUS.FAILED:
122
+ self.connect_request_id = connection_request.connectedAccountId
123
+ connected_account = self.toolset.get_connected_account(id=self.connect_request_id)
124
+
125
+ if connected_account.status is not COMPOSIO_STATUS.FAILED:
126
+ setattr(self.toolset, "entity_id", self.user_id)
127
+
128
+ return connected_account, connected_account.status if connected_account else connection_request.connectionStatus
129
+
130
+
131
+
132
+
133
+ # @action(toolname=ComposioAppName.HUBSPOT)
134
+ # def deploy(self, param1: str, param2: str, execute_request: Callable) -> str:
135
+ # """
136
+ # Define custom actions
137
+ # my custom action description which will be passed to llm
138
+
139
+ # :param param1: param1 description which will be passed to llm
140
+ # :param param2: param2 description which will be passed to llm
141
+ # :return info: return description
142
+ # """
143
+
144
+ # response = execute_request(
145
+ # "/my_action_endpoint",
146
+ # "GET",
147
+ # {} # body can be added here
148
+ # ) # execute requests by appending credentials to the request
149
+ # return str(response) # complete auth dict is available for local use if needed
@@ -1,5 +1,6 @@
1
1
  from typing import Callable
2
2
  from pydantic import BaseModel
3
+
3
4
  from versionhq.tool.model import Tool
4
5
 
5
6
 
@@ -13,6 +14,7 @@ def tool(*args):
13
14
  def _make_tool(f: Callable) -> Tool:
14
15
  if f.__doc__ is None:
15
16
  raise ValueError("Function must have a docstring")
17
+
16
18
  if f.__annotations__ is None:
17
19
  raise ValueError("Function must have type annotations")
18
20
 
@@ -26,7 +28,7 @@ def tool(*args):
26
28
  },
27
29
  },
28
30
  )
29
- return Tool(name=tool_name, func=f, args_schema=args_schema)
31
+ return Tool(name=tool_name, function=f, args_schema=args_schema)
30
32
 
31
33
  return _make_tool
32
34