versionhq 1.2.4.3__py3-none-any.whl → 1.2.4.6__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.
Files changed (34) hide show
  1. versionhq/__init__.py +12 -3
  2. versionhq/_prompt/auto_feedback.py +2 -2
  3. versionhq/_prompt/model.py +24 -29
  4. versionhq/_utils/__init__.py +2 -0
  5. versionhq/_utils/convert_img_url.py +15 -0
  6. versionhq/_utils/is_valid_enum.py +25 -0
  7. versionhq/_utils/llm_as_a_judge.py +0 -1
  8. versionhq/_utils/usage_metrics.py +35 -14
  9. versionhq/agent/inhouse_agents.py +2 -2
  10. versionhq/agent/model.py +100 -29
  11. versionhq/agent_network/formation.py +6 -12
  12. versionhq/agent_network/model.py +4 -5
  13. versionhq/clients/customer/__init__.py +2 -2
  14. versionhq/clients/product/model.py +4 -4
  15. versionhq/clients/workflow/model.py +1 -1
  16. versionhq/llm/llm_vars.py +7 -6
  17. versionhq/llm/model.py +3 -1
  18. versionhq/storage/task_output_storage.py +2 -2
  19. versionhq/task/model.py +112 -100
  20. versionhq/task_graph/draft.py +4 -4
  21. versionhq/task_graph/model.py +34 -30
  22. versionhq/tool/composio/__init__.py +0 -0
  23. versionhq/tool/{composio_tool.py → composio/model.py} +4 -5
  24. versionhq/tool/gpt/__init__.py +6 -0
  25. versionhq/tool/gpt/_enum.py +28 -0
  26. versionhq/tool/gpt/cup.py +145 -0
  27. versionhq/tool/gpt/file_search.py +163 -0
  28. versionhq/tool/gpt/web_search.py +89 -0
  29. {versionhq-1.2.4.3.dist-info → versionhq-1.2.4.6.dist-info}/METADATA +4 -4
  30. {versionhq-1.2.4.3.dist-info → versionhq-1.2.4.6.dist-info}/RECORD +34 -26
  31. /versionhq/tool/{composio_tool_vars.py → composio/params.py} +0 -0
  32. {versionhq-1.2.4.3.dist-info → versionhq-1.2.4.6.dist-info}/LICENSE +0 -0
  33. {versionhq-1.2.4.3.dist-info → versionhq-1.2.4.6.dist-info}/WHEEL +0 -0
  34. {versionhq-1.2.4.3.dist-info → versionhq-1.2.4.6.dist-info}/top_level.txt +0 -0
versionhq/__init__.py CHANGED
@@ -24,7 +24,10 @@ from versionhq.tool.model import Tool, ToolSet
24
24
  from versionhq.tool.rag_tool import RagTool
25
25
  from versionhq.tool.cache_handler import CacheHandler
26
26
  from versionhq.tool.tool_handler import ToolHandler
27
- from versionhq.tool.composio_tool import ComposioHandler
27
+ from versionhq.tool.composio.model import ComposioBaseTool
28
+ from versionhq.tool.gpt.cup import GPTToolCUP, CUPToolSchema
29
+ from versionhq.tool.gpt.file_search import GPTToolFileSearch, FilterSchema
30
+ from versionhq.tool.gpt.web_search import GPTToolWebSearch
28
31
  from versionhq.memory.contextual_memory import ContextualMemory
29
32
  from versionhq.memory.model import ShortTermMemory,LongTermMemory, UserMemory, MemoryItem
30
33
 
@@ -32,7 +35,7 @@ from versionhq.agent_network.formation import form_agent_network
32
35
  from versionhq.task_graph.draft import workflow
33
36
 
34
37
 
35
- __version__ = "1.2.4.3"
38
+ __version__ = "1.2.4.6"
36
39
  __all__ = [
37
40
  "Agent",
38
41
 
@@ -85,7 +88,13 @@ __all__ = [
85
88
  "RagTool",
86
89
  "CacheHandler",
87
90
  "ToolHandler",
88
- "ComposioHandler",
91
+ "ComposioBaseTool",
92
+
93
+ "GPTToolCUP",
94
+ "CUPToolSchema",
95
+ "GPTToolFileSearch",
96
+ "FilterSchema",
97
+ "GPTToolWebSearch",
89
98
 
90
99
  "ContextualMemory",
91
100
  "ShortTermMemory",
@@ -5,7 +5,7 @@ from pydantic import InstanceOf, Field
5
5
 
6
6
  from versionhq.agent.model import Agent
7
7
  from versionhq.task.model import Task
8
- from versionhq.task_graph.model import TaskGraph, Node, DependencyType, ReformTriggerEvent
8
+ from versionhq.task_graph.model import TaskGraph, Node, DependencyType
9
9
  from versionhq._prompt.model import Prompt
10
10
  from versionhq._prompt.constants import REFLECT, INTEGRATE, parameter_sets
11
11
 
@@ -67,7 +67,7 @@ class PromptFeedbackGraph(TaskGraph):
67
67
  if not agents:
68
68
  return None
69
69
 
70
- self.concl_template = base_task.pydantic_output if base_task.pydantic_output else base_task.response_fields if base_task.response_fields else None
70
+ self.concl_response_schema = base_task.response_schema
71
71
  base_agent.callbacks.append(self._reflect)
72
72
  init_node = Node(task=base_task, assigned_to=base_agent)
73
73
  self.add_node(init_node)
@@ -2,9 +2,9 @@
2
2
  from typing import Dict, List, Tuple, Any
3
3
  from textwrap import dedent
4
4
 
5
- from pydantic import InstanceOf
5
+ from pydantic import InstanceOf, BaseModel
6
6
 
7
- from versionhq._utils import is_valid_url
7
+ from versionhq._utils import is_valid_url, convert_img_url
8
8
 
9
9
 
10
10
  class Prompt:
@@ -25,34 +25,26 @@ class Prompt:
25
25
 
26
26
 
27
27
  def _draft_output_prompt(self) -> str:
28
- """Drafts prompt for output either from `pydantic_output` or `response_fields`"""
28
+ """Drafts prompt for output format using `response_schema`."""
29
29
 
30
- from versionhq.llm.model import DEFAULT_MODEL_PROVIDER_NAME
30
+ from versionhq.task.model import ResponseField
31
31
 
32
32
  output_prompt = ""
33
- model_provider = self.agent.llm.provider if self.agent else DEFAULT_MODEL_PROVIDER_NAME
33
+ output_formats_to_follow = dict()
34
34
 
35
- if self.task.pydantic_output:
36
- output_prompt, output_formats_to_follow = "", dict()
37
- response_format = str(self.task._structure_response_format(model_provider=model_provider))
38
- for k, v in self.task.pydantic_output.model_fields.items():
39
- output_formats_to_follow[k] = f"<Return your answer in {v.annotation}>"
35
+ if self.task.response_schema:
36
+ if isinstance(self.task.response_schema, list):
37
+ for item in self.task.response_schema:
38
+ if isinstance(item, ResponseField):
39
+ output_formats_to_follow[item.title] = f"<Return your answer in {item.data_type.__name__}>"
40
40
 
41
- output_prompt = f"""Your response MUST be a valid JSON string that strictly follows the response format. Use double quotes for all keys and string values. Do not use single quotes, trailing commas, or any other non-standard JSON syntax.
42
- Response format: {response_format}
43
- Ref. Output image: {output_formats_to_follow}
44
- """
45
- elif self.task.response_fields:
46
- output_prompt, output_formats_to_follow = "", dict()
47
- response_format = str(self.task._structure_response_format(model_provider=model_provider))
48
- for item in self.task.response_fields:
49
- if item:
50
- output_formats_to_follow[item.title] = f"<Return your answer in {item.data_type.__name__}>"
41
+ elif issubclass(self.task.response_schema, BaseModel):
42
+ for k, v in self.task.response_schema.model_fields.items():
43
+ output_formats_to_follow[k] = f"<Return your answer in {v.annotation}>"
51
44
 
52
45
  output_prompt = f"""Your response MUST be a valid JSON string that strictly follows the response format. Use double quotes for all keys and string values. Do not use single quotes, trailing commas, or any other non-standard JSON syntax.
53
- Response format: {response_format}
54
46
  Ref. Output image: {output_formats_to_follow}
55
- """
47
+ """
56
48
  else:
57
49
  output_prompt = "You MUST return your response as a valid JSON serializable string, enclosed in double quotes. Use double quotes for all keys and string values. Do NOT use single quotes, trailing commas, or other non-standard JSON syntax."
58
50
 
@@ -107,12 +99,9 @@ Ref. Output image: {output_formats_to_follow}
107
99
  content_messages = {}
108
100
 
109
101
  if self.task.image:
110
- with open(self.task.image, "rb") as file:
111
- content = file.read()
112
- if content:
113
- encoded_file = base64.b64encode(content).decode("utf-8")
114
- img_url = f"data:image/jpeg;base64,{encoded_file}"
115
- content_messages.update({ "type": "image_url", "image_url": { "url": img_url }})
102
+ img_url = convert_img_url(self.task.image)
103
+ if img_url:
104
+ content_messages.update({ "type": "image_url", "image_url": { "url": img_url }})
116
105
 
117
106
  if self.task.file:
118
107
  if is_valid_url(self.task.file):
@@ -154,7 +143,7 @@ Ref. Output image: {output_formats_to_follow}
154
143
  return "\n".join(task_slices)
155
144
 
156
145
 
157
- def format_core(self, rag_tools: List[Any] = None) -> Tuple[str, str, List[Dict[str, str]]]:
146
+ def format_core(self, rag_tools: List[Any] = None, gpt_tools: List[Any] = None) -> Tuple[str, str, List[Dict[str, str]]]:
158
147
  """Formats prompt messages sent to the LLM, then returns task prompt, developer prompt, and messages."""
159
148
 
160
149
  from versionhq.knowledge._utils import extract_knowledge_context
@@ -176,6 +165,12 @@ Ref. Output image: {output_formats_to_follow}
176
165
  if rag_tool_context:
177
166
  user_prompt += ",".join(rag_tool_context) if isinstance(rag_tool_context, list) else str(rag_tool_context)
178
167
 
168
+ if gpt_tools:
169
+ for item in gpt_tools:
170
+ raw, _, _ = item.run()
171
+ if raw:
172
+ user_prompt += str(raw)
173
+
179
174
  if self.agent.with_memory == True:
180
175
  contextual_memory = ContextualMemory(
181
176
  memory_config=self.agent.memory_config, stm=self.agent.short_term_memory, ltm=self.agent.long_term_memory, um=self.agent.user_memory
@@ -3,3 +3,5 @@ from versionhq._utils.process_config import process_config
3
3
  from versionhq._utils.vars import KNOWLEDGE_DIRECTORY, MAX_FILE_NAME_LENGTH
4
4
  from versionhq._utils.is_valid_url import is_valid_url
5
5
  from versionhq._utils.usage_metrics import UsageMetrics, ErrorType
6
+ from versionhq._utils.convert_img_url import convert_img_url
7
+ from versionhq._utils.is_valid_enum import is_valid_enum
@@ -0,0 +1,15 @@
1
+ import base64
2
+
3
+ def convert_img_url(img_url: str) -> str | None:
4
+ try:
5
+ with open(img_url, "rb") as file:
6
+ content = file.read()
7
+ if content:
8
+ encoded_file = base64.b64encode(content).decode("utf-8")
9
+ img_url = f"data:image/jpeg;base64,{encoded_file}"
10
+ return img_url
11
+
12
+ else: return None
13
+
14
+ except:
15
+ return None
@@ -0,0 +1,25 @@
1
+ from enum import Enum, IntEnum
2
+ from typing import Any
3
+
4
+
5
+ def is_valid_enum(enum: Enum | IntEnum, key: str = None, val: str | Enum | IntEnum = None) -> bool:
6
+ if not enum: return False
7
+
8
+ if key:
9
+ key = key.upper()
10
+ matched = [k for k in enum._member_map_.keys() if hasattr(enum, "_member_map_") and k == key]
11
+ return bool(matched)
12
+
13
+ elif val:
14
+ match val:
15
+ case str():
16
+ matched = [k for k in enum._value2member_map_.keys() if hasattr(enum, "_value2member_map_") and k == val]
17
+ return bool(matched)
18
+
19
+ case Enum() | IntEnum():
20
+ return val in enum
21
+
22
+ case _:
23
+ return False
24
+
25
+ else: return False
@@ -2,7 +2,6 @@ import json
2
2
  import numpy as np
3
3
  from sklearn.metrics import precision_score, recall_score, roc_auc_score, cohen_kappa_score
4
4
  from typing import List, Tuple, Dict, Any
5
- from pathlib import Path
6
5
 
7
6
 
8
7
  class LLMJudge:
@@ -1,13 +1,13 @@
1
1
  import uuid
2
- import enum
3
2
  import datetime
3
+ from enum import IntEnum
4
4
  from typing import Dict, List
5
5
  from typing_extensions import Self
6
6
 
7
7
  from pydantic import BaseModel, UUID4, InstanceOf
8
8
 
9
9
 
10
- class ErrorType(enum.Enum):
10
+ class ErrorType(IntEnum):
11
11
  FORMAT = 1
12
12
  TOOL = 2
13
13
  API = 3
@@ -22,19 +22,38 @@ class UsageMetrics(BaseModel):
22
22
  total_tokens: int = 0
23
23
  prompt_tokens: int = 0
24
24
  completion_tokens: int = 0
25
+ input_tokens: int = 0
26
+ output_tokens: int = 0
25
27
  successful_requests: int = 0
26
28
  total_errors: int = 0
27
29
  error_breakdown: Dict[ErrorType, int] = dict()
28
30
  latency: float = 0.0 # in ms
29
31
 
30
- def record_token_usage(self, token_usages: List[Dict[str, int]]) -> None:
32
+
33
+ def record_token_usage(self, *args, **kwargs) -> None:
31
34
  """Records usage metrics from the raw response of the model."""
32
35
 
33
- if token_usages:
34
- for item in token_usages:
35
- self.total_tokens += int(item["total_tokens"]) if "total_tokens" in item else 0
36
- self.completion_tokens += int(item["completion_tokens"]) if "completion_tokens" in item else 0
37
- self.prompt_tokens += int(item["prompt_tokens"]) if "prompt_tokens" in item else 0
36
+ if args:
37
+ for item in args:
38
+ match item:
39
+ case dict():
40
+ if hasattr(self, k):
41
+ setattr(self, k, int(getattr(self, k)) + int(v))
42
+ case UsageMetrics():
43
+ self = self.aggregate(metrics=item)
44
+ case _:
45
+ try:
46
+ self.completion_tokens += item.completion_tokens if hasattr(item, "completion_tokens") else 0
47
+ self.prompt_tokens += item.prompt_tokens if hasattr(item, "prompt_tokens") else 0
48
+ self.total_tokens += item.total_tokens if hasattr(item, "total_tokens") else 0
49
+ self.input_tokens += item.input_tokens if hasattr(item, "input_tokens") else 0
50
+ self.output_tokens += item.output_tokens if hasattr(item, "output_tokens") else 0
51
+ except:
52
+ pass
53
+ if kwargs:
54
+ for k, v in kwargs.items():
55
+ if hasattr(self, k):
56
+ setattr(self, k, int(getattr(self, k)) + int(v))
38
57
 
39
58
 
40
59
  def record_errors(self, type: ErrorType = None) -> None:
@@ -54,12 +73,14 @@ class UsageMetrics(BaseModel):
54
73
  if not metrics:
55
74
  return self
56
75
 
57
- self.total_tokens += metrics.total_tokens if metrics.total_tokens else 0
58
- self.prompt_tokens += metrics.prompt_tokens if metrics.prompt_tokens else 0
59
- self.completion_tokens += metrics.completion_tokens if metrics.completion_tokens else 0
60
- self.successful_requests += metrics.successful_requests if metrics.successful_requests else 0
61
- self.total_errors += metrics.total_errors if metrics.total_errors else 0
62
- self.latency += metrics.latency if metrics.latency else 0.0
76
+ self.total_tokens += metrics.total_tokens
77
+ self.prompt_tokens += metrics.prompt_tokens
78
+ self.completion_tokens += metrics.completion_tokens
79
+ self.input_tokens += metrics.input_tokens
80
+ self.output_tokens += metrics.output_tokens
81
+ self.successful_requests += metrics.successful_requests
82
+ self.total_errors += metrics.total_errors
83
+ self.latency += metrics.latency
63
84
  self.latency = round(self.latency, 3)
64
85
 
65
86
  if metrics.error_breakdown:
@@ -30,7 +30,7 @@ vhq_task_evaluator = Agent(
30
30
  vhq_formation_planner = Agent(
31
31
  role="vhq-Formation Planner",
32
32
  goal="Plan a formation of agents based on the given task descirption.",
33
- llm="gemini/gemini-2.0-flash-exp",
33
+ llm="gemini/gemini-2.0-flash",
34
34
  llm_config=dict(top_p=0.8, topK=40, temperature=0.9),
35
35
  maxit=1,
36
36
  max_retry_limit=1,
@@ -46,7 +46,7 @@ vhq_formation_planner = Agent(
46
46
  vhq_agent_creator = Agent(
47
47
  role="vhq-Agent Creator",
48
48
  goal="build an agent that can handle the given task",
49
- llm="gemini/gemini-2.0-flash-exp",
49
+ llm="gemini/gemini-2.0-flash",
50
50
  maxit=1,
51
51
  max_retry_limit=1,
52
52
  )
versionhq/agent/model.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import os
2
2
  import uuid
3
- from typing import Any, Dict, List, Optional, TypeVar, Callable, Type
3
+ from typing import Any, Dict, List, Optional, TypeVar, Callable, Type, Tuple
4
4
  from typing_extensions import Self
5
5
  from dotenv import load_dotenv
6
6
 
@@ -11,7 +11,7 @@ from versionhq.agent.rpm_controller import RPMController
11
11
  from versionhq.tool.model import Tool, ToolSet, BaseTool
12
12
  from versionhq.knowledge.model import BaseKnowledgeSource, Knowledge
13
13
  from versionhq.memory.model import ShortTermMemory, LongTermMemory, UserMemory
14
- from versionhq._utils import Logger, process_config, is_valid_url, ErrorType
14
+ from versionhq._utils import Logger, process_config, is_valid_url, ErrorType, UsageMetrics
15
15
 
16
16
 
17
17
  load_dotenv(override=True)
@@ -124,6 +124,9 @@ class Agent(BaseModel):
124
124
  Similar to the LLM set up, when the agent has tools, we will declare them using the Tool class.
125
125
  """
126
126
  from versionhq.tool.rag_tool import RagTool
127
+ from versionhq.tool.gpt.web_search import GPTToolWebSearch
128
+ from versionhq.tool.gpt.file_search import GPTToolFileSearch
129
+ from versionhq.tool.gpt.cup import GPTToolCUP
127
130
 
128
131
  if not self.tools:
129
132
  return self
@@ -131,7 +134,7 @@ class Agent(BaseModel):
131
134
  tool_list = []
132
135
  for item in self.tools:
133
136
  match item:
134
- case RagTool() | BaseTool():
137
+ case RagTool() | BaseTool() | GPTToolCUP() | GPTToolFileSearch() | GPTToolWebSearch():
135
138
  tool_list.append(item)
136
139
 
137
140
  case Tool():
@@ -353,8 +356,8 @@ class Agent(BaseModel):
353
356
  response_format: Optional[Dict[str, Any]] = None,
354
357
  tools: Optional[List[InstanceOf[Tool]| InstanceOf[ToolSet] | Type[Tool]]] = None,
355
358
  tool_res_as_final: bool = False,
356
- task: Any = None
357
- ) -> Dict[str, Any]:
359
+ # task: Any = None
360
+ ) -> Tuple[str, UsageMetrics]:
358
361
  """
359
362
  Create formatted prompts using the developer prompt and the agent's backstory, then call the base model.
360
363
  - Execute the task up to `self.max_retry_limit` times in case of receiving an error or empty response.
@@ -364,6 +367,7 @@ class Agent(BaseModel):
364
367
  task_execution_counter = 0
365
368
  iterations = 0
366
369
  raw_response = None
370
+ usage = UsageMetrics()
367
371
 
368
372
  try:
369
373
  if self._rpm_controller and self.max_rpm:
@@ -373,17 +377,17 @@ class Agent(BaseModel):
373
377
 
374
378
  if tool_res_as_final:
375
379
  raw_response = self.func_calling_llm.call(messages=messages, tools=tools, tool_res_as_final=True)
376
- task._usage.record_token_usage(token_usages=self.func_calling_llm._usages)
380
+ usage.record_token_usage(*self.func_calling_llm._usages)
377
381
  else:
378
382
  raw_response = self.llm.call(messages=messages, response_format=response_format, tools=tools)
379
- task._usage.record_token_usage(token_usages=self.llm._usages)
383
+ usage.record_token_usage(*self.llm._usages)
380
384
 
381
385
  task_execution_counter += 1
382
386
  Logger(**self._logger_config, filename=self.key).log(level="info", message=f"Agent response: {raw_response}", color="green")
383
- return raw_response
387
+ return raw_response, usage
384
388
 
385
389
  except Exception as e:
386
- task._usage.record_errors(type=ErrorType.API)
390
+ usage.record_errors(type=ErrorType.API)
387
391
  Logger(**self._logger_config, filename=self.key).log(level="error", message=f"An error occured. The agent will retry: {str(e)}", color="red")
388
392
 
389
393
  while not raw_response and task_execution_counter <= self.max_retry_limit:
@@ -392,12 +396,12 @@ class Agent(BaseModel):
392
396
  self._rpm_controller.check_or_wait()
393
397
 
394
398
  raw_response = self.llm.call(messages=messages, response_format=response_format, tools=tools)
395
- task._tokens = self.llm._tokens
399
+ usage.record_token_usage(*self.llm._usages)
396
400
  iterations += 1
397
401
 
398
402
  task_execution_counter += 1
399
403
  Logger(**self._logger_config, filename=self.key).log(level="info", message=f"Agent #{task_execution_counter} response: {raw_response}", color="green")
400
- return raw_response
404
+ return raw_response, usage
401
405
 
402
406
  if not raw_response:
403
407
  Logger(**self._logger_config, filename=self.key).log(level="error", message="Received None or empty response from the model", color="red")
@@ -423,6 +427,57 @@ class Agent(BaseModel):
423
427
  return self.set_up_llm()
424
428
 
425
429
 
430
+ def _sort_tools(self, task = None) -> Tuple[List[Any], List[Any], List[Any]]:
431
+ """Sorts agent and task tools by class."""
432
+
433
+ from versionhq.tool.rag_tool import RagTool
434
+ from versionhq.tool.gpt.web_search import GPTToolWebSearch
435
+ from versionhq.tool.gpt.file_search import GPTToolFileSearch
436
+ from versionhq.tool.gpt.cup import GPTToolCUP
437
+
438
+ all_tools = []
439
+ if task: all_tools = task.tools + self.tools if task.can_use_agent_tools else task.tools
440
+ else: all_tools = self.tools
441
+
442
+ rag_tools, gpt_tools, tools = [], [], []
443
+ if all_tools:
444
+ for item in all_tools:
445
+ match item:
446
+ case RagTool():
447
+ rag_tools.append(item)
448
+
449
+ case GPTToolCUP() | GPTToolFileSearch() | GPTToolWebSearch():
450
+ gpt_tools.append(item)
451
+
452
+ case Tool() | BaseTool() | ToolSet():
453
+ tools.append(item)
454
+
455
+ return rag_tools, gpt_tools, tools
456
+
457
+
458
+ def _handle_gpt_tools(self, gpt_tools: list[Any] = None) -> Any: # TaskOutput
459
+ """Generates k, v pairs from multiple GPT tool results and stores them in TaskOutput class."""
460
+
461
+ from versionhq.task.model import TaskOutput
462
+ from versionhq._utils import UsageMetrics
463
+
464
+ if not gpt_tools:
465
+ return
466
+
467
+ tool_res = dict()
468
+ annotation_set = dict()
469
+ total_usage = UsageMetrics()
470
+
471
+ for i, item in enumerate(gpt_tools):
472
+ raw, annotations, usage = item.run()
473
+ tool_res.update({ str(i): raw })
474
+ annotation_set.update({ str(i): annotations })
475
+ total_usage.aggregate(metrics=usage)
476
+
477
+ res = TaskOutput(raw=str(tool_res), tool_output=tool_res, usage=total_usage, annotations=annotation_set)
478
+ return res
479
+
480
+
426
481
  def update(self, **kwargs) -> Self:
427
482
  """
428
483
  Update the existing agent. Address variables that require runnning set_up_x methods first, then update remaining variables.
@@ -475,15 +530,28 @@ class Agent(BaseModel):
475
530
  return self
476
531
 
477
532
 
478
- def start(self, context: Any = None, tool_res_as_final: bool = False, image: str = None, file: str = None, audio: str = None) -> Any | None:
533
+ def start(
534
+ self,
535
+ context: Any = None,
536
+ tool_res_as_final: bool = False,
537
+ image: str = None,
538
+ file: str = None,
539
+ audio: str = None
540
+ ) -> Any:
479
541
  """
480
- Defines and executes a task when it is not given and returns TaskOutput object.
542
+ Defines and executes a task, then returns TaskOutput object with the generated task.
481
543
  """
482
544
 
545
+ from versionhq.task.model import Task
546
+
483
547
  if not self.role:
484
- return None
548
+ return None, None
485
549
 
486
- from versionhq.task.model import Task
550
+ _, gpt_tools, _ = self._sort_tools()
551
+
552
+ if gpt_tools and tool_res_as_final == True:
553
+ res = self._handle_gpt_tools(gpt_tools=gpt_tools)
554
+ return res
487
555
 
488
556
  class Output(BaseModel):
489
557
  result: str
@@ -491,49 +559,52 @@ class Agent(BaseModel):
491
559
 
492
560
  task = Task(
493
561
  description=f"Generate a simple result in a sentence to achieve the goal: {self.goal if self.goal else self.role}. If needed, list up necessary steps in concise manner.",
494
- pydantic_output=Output,
562
+ response_schema=Output,
495
563
  tool_res_as_final=tool_res_as_final,
496
564
  image=image, #REFINEME - query memory/knowledge or self create
497
565
  file=file,
498
566
  audio=audio,
567
+ can_use_agent_tools=True if self.tools else False,
499
568
  )
500
569
  res = task.execute(agent=self, context=context)
501
570
  return res
502
571
 
503
572
 
504
- def execute_task(self, task, context: Optional[Any] = None, task_tools: Optional[List[Tool | ToolSet]] = list()) -> str:
573
+ def execute_task(self, task, context: Optional[Any] = None) -> Tuple[str, str, Any, UsageMetrics]:
505
574
  """Handling task execution."""
506
575
 
507
- from versionhq.task.model import Task
508
- from versionhq.tool.rag_tool import RagTool
509
576
  from versionhq._prompt.model import Prompt
577
+ from versionhq.task.model import Task
510
578
 
511
579
  task: InstanceOf[Task] = task
512
- all_tools: Optional[List[Tool | ToolSet | RagTool | Type[BaseTool]]] = task_tools + self.tools if task.can_use_agent_tools else task_tools
513
- rag_tools: Optional[List[RagTool]] = [item for item in all_tools if isinstance(item, RagTool)] if all_tools else None
514
- tools: Optional[List[Tool | ToolSet | RagTool | Type[BaseTool]]] = [item for item in all_tools if not isinstance(item, RagTool)] if all_tools else None
580
+ rag_tools, gpt_tools, tools = self._sort_tools(task=task)
581
+ raw_response = ""
582
+ user_prompt, dev_prompt = "", ""
583
+ usage = UsageMetrics(id=task.id)
515
584
 
516
585
  if self.max_rpm and self._rpm_controller:
517
586
  self._rpm_controller._reset_request_count()
518
587
 
519
- user_prompt, dev_prompt, messages = Prompt(task=task, agent=self, context=context).format_core(rag_tools=rag_tools)
588
+ if task.tool_res_as_final == True and gpt_tools:
589
+ self._times_executed += 1
590
+ res = self._handle_gpt_tools(gpt_tools=gpt_tools)
591
+ return user_prompt, dev_prompt, res, res.usage
592
+
593
+ user_prompt, dev_prompt, messages = Prompt(task=task, agent=self, context=context).format_core(rag_tools=rag_tools, gpt_tools=gpt_tools)
520
594
 
521
595
  try:
522
596
  self._times_executed += 1
523
- raw_response = self._invoke(
597
+ raw_response, usage = self._invoke(
524
598
  messages=messages,
525
599
  response_format=task._structure_response_format(model_provider=self.llm.provider),
526
600
  tools=tools,
527
601
  tool_res_as_final=task.tool_res_as_final,
528
- task=task
529
602
  )
530
- if raw_response:
531
- task._usage.successful_requests += 1
532
603
 
533
604
  except Exception as e:
534
605
  self._times_executed += 1
535
606
  Logger(**self._logger_config, filename=self.key).log(level="error", message=f"The agent failed to execute the task. Error: {str(e)}", color="red")
536
- user_prompt, dev_prompt, raw_response = self.execute_task(task, context, task_tools)
607
+ user_prompt, dev_prompt, raw_response, usage = self.execute_task(task, context)
537
608
 
538
609
  if self._times_executed > self.max_retry_limit:
539
610
  Logger(**self._logger_config, filename=self.key).log(level="error", message=f"Max retry limit has exceeded.", color="red")
@@ -542,7 +613,7 @@ class Agent(BaseModel):
542
613
  if self.max_rpm and self._rpm_controller:
543
614
  self._rpm_controller.stop_rpm_counter()
544
615
 
545
- return user_prompt, dev_prompt, raw_response
616
+ return user_prompt, dev_prompt, raw_response, usage
546
617
 
547
618
 
548
619
  @property
@@ -7,7 +7,7 @@ from versionhq.task.model import Task
7
7
  from versionhq.agent.model import Agent
8
8
  from versionhq.agent_network.model import AgentNetwork, Member, Formation
9
9
  from versionhq.agent.inhouse_agents import vhq_formation_planner
10
- from versionhq._utils import Logger
10
+ from versionhq._utils import Logger, is_valid_enum
11
11
 
12
12
  import chromadb
13
13
  chromadb.api.client.SharedSystemClient.clear_system_cache()
@@ -74,8 +74,8 @@ def form_agent_network(
74
74
  leader_agent: str
75
75
 
76
76
  vhq_task = Task(
77
- description=f"Design a team of specialized agents to fully automate the following task and achieve the expected outcome. For each agent, define its role, task description, and expected outputs via the task with items in a list. Then specify the formation if the formation is not given. If you think SUPERVISING or HYBRID is the best formation, include a leader_agent role, else leave the leader_agent role blank.\nTask: {str(task)}\nExpected outcome: {prompt_expected_outcome}\nFormation: {prompt_formation}",
78
- pydantic_output=Outcome
77
+ description=f"Design a team of specialized agents to fully automate the following task and deliver the expected outcome. For each agent, define its role, task description, and expected outputs via the task with items in a list. Then specify the formation if the formation is not given. If you think SUPERVISING or HYBRID is the best formation, include a leader_agent role, else leave the leader_agent role blank.\nTask: {str(task)}\nExpected outcome: {prompt_expected_outcome}\nFormation: {prompt_formation}",
78
+ response_schema=Outcome
79
79
  )
80
80
 
81
81
  if agents:
@@ -83,14 +83,6 @@ def form_agent_network(
83
83
 
84
84
  res = vhq_task.execute(agent=vhq_formation_planner, context=context)
85
85
 
86
- formation_keys = []
87
- if hasattr(res.pydantic, "formation"):
88
- formation_keys = [k for k in Formation._member_map_.keys() if k == res.pydantic.formation.upper()]
89
- elif "formation" in res.json_dict:
90
- formation_keys = [k for k in Formation._member_map_.keys() if k == res.json_dict["formation"].upper()]
91
-
92
- _formation = Formation[formation_keys[0]] if formation_keys else Formation.SUPERVISING
93
-
94
86
  network_tasks = []
95
87
  members = []
96
88
  leader = res._fetch_value_of(key="leader_agent")
@@ -98,6 +90,8 @@ def form_agent_network(
98
90
  created_agents = [Agent(role=str(item), goal=str(item)) for item in agent_roles] if agent_roles else []
99
91
  task_descriptions = res._fetch_value_of(key="task_descriptions")
100
92
  task_outcomes = res._fetch_value_of(key="task_outcomes")
93
+ formation_key = res.json_dict["formation"] if "formation" in res.json_dict else None
94
+ _formation = Formation[formation_key] if is_valid_enum(key=formation_key, enum=Formation) else Formation.SUPERVISING
101
95
 
102
96
  if agents:
103
97
  for i, agent in enumerate(created_agents):
@@ -120,7 +114,7 @@ def form_agent_network(
120
114
  except:
121
115
  pass
122
116
  output = create_model("Output", **fields) if fields else None
123
- _task = Task(description=task_descriptions[i], pydantic_output=output)
117
+ _task = Task(description=task_descriptions[i], response_schema=output)
124
118
  created_tasks.append(_task)
125
119
 
126
120
  if len(created_tasks) <= len(created_agents):
@@ -1,7 +1,6 @@
1
1
  import uuid
2
2
  import warnings
3
- from enum import Enum
4
- from concurrent.futures import Future
3
+ from enum import IntEnum
5
4
  from hashlib import md5
6
5
  from typing import Any, Dict, List, Callable, Optional, Tuple
7
6
  from typing_extensions import Self
@@ -30,7 +29,7 @@ GenerateSchema.match_type = match_type
30
29
  warnings.filterwarnings("ignore", category=SyntaxWarning, module="pysbd")
31
30
 
32
31
 
33
- class Formation(str, Enum):
32
+ class Formation(IntEnum):
34
33
  SOLO = 1
35
34
  SUPERVISING = 2
36
35
  SQUAD = 3
@@ -38,7 +37,7 @@ class Formation(str, Enum):
38
37
  HYBRID = 10
39
38
 
40
39
 
41
- class TaskHandlingProcess(str, Enum):
40
+ class TaskHandlingProcess(IntEnum):
42
41
  """
43
42
  A class representing task handling processes to tackle multiple tasks.
44
43
  When the agent network has multiple tasks that connect with edges, follow the edge conditions.
@@ -206,7 +205,7 @@ class AgentNetwork(BaseModel):
206
205
  for unassgined_task in unassigned_tasks:
207
206
  task = Task(
208
207
  description=f"Based on the following task summary, draft an agent's role and goal in concise manner. Task summary: {unassgined_task.summary}",
209
- response_fields=[
208
+ response_schema=[
210
209
  ResponseField(title="goal", data_type=str, required=True),
211
210
  ResponseField(title="role", data_type=str, required=True),
212
211
  ],
@@ -1,7 +1,7 @@
1
- from enum import Enum
1
+ from enum import IntEnum
2
2
 
3
3
 
4
- class Status(str, Enum):
4
+ class Status(IntEnum):
5
5
  NOT_ASSIGNED = 0
6
6
  READY_TO_DEPLOY = 1
7
7
  ACTIVE_ON_WORKFLOW = 2