versionhq 1.1.6.4__py3-none-any.whl → 1.1.7.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
versionhq/task/model.py CHANGED
@@ -3,16 +3,10 @@ import threading
3
3
  import uuid
4
4
  from concurrent.futures import Future
5
5
  from hashlib import md5
6
- from typing import Any, Dict, List, Set, Optional, Tuple, Callable
7
-
8
- from pydantic import (
9
- UUID4,
10
- BaseModel,
11
- Field,
12
- PrivateAttr,
13
- field_validator,
14
- model_validator,
15
- )
6
+ from typing import Any, Dict, List, Set, Optional, Tuple, Callable, Union, Type
7
+ from typing_extensions import Annotated
8
+
9
+ from pydantic import UUID4, BaseModel, Field, PrivateAttr, field_validator, model_validator, create_model
16
10
  from pydantic_core import PydanticCustomError
17
11
 
18
12
  from versionhq._utils.process_config import process_config
@@ -26,62 +20,73 @@ class ResponseField(BaseModel):
26
20
  """
27
21
 
28
22
  title: str = Field(default=None)
29
- type: str = Field(default=None)
23
+ type: Type = Field(default=str)
30
24
  required: bool = Field(default=True)
31
25
 
32
26
 
27
+ def _annotate(self, value: Any) -> Annotated:
28
+ """
29
+ Address `create_model`
30
+ """
31
+ return Annotated[self.type, value] if isinstance(value, self.type) else Annotated[str, str(value)]
32
+
33
+
34
+ def _convert(self, value: Any) -> Any:
35
+ try:
36
+ if self.type is Any:
37
+ pass
38
+ elif self.type is int:
39
+ return int(value)
40
+ elif self.type is float:
41
+ return float(value)
42
+ elif self.type is list or self.type is dict:
43
+ return json.loads(value)
44
+ else:
45
+ return value
46
+ except:
47
+ return value
48
+
49
+
50
+ def create_pydantic_model(self, result: Dict, base_model: Union[BaseModel | Any]) -> Any:
51
+ for k, v in result.items():
52
+ if k is not self.title:
53
+ pass
54
+ elif type(v) is not self.type:
55
+ v = self._convert(v)
56
+ setattr(base_model, k, v)
57
+ else:
58
+ setattr(base_model, k, v)
59
+ return base_model
60
+
61
+
62
+ class AgentOutput(BaseModel):
63
+ """
64
+ Keep adding agents' learning and recommendation and store it in `pydantic` field of `TaskOutput` class.
65
+ Since the TaskOutput class has `agent` field, we don't add any info on the agent that handled the task.
66
+ """
67
+ customer_id: str = Field(default=None, max_length=126, description="customer uuid")
68
+ customer_analysis: str = Field(default=None, max_length=256, description="analysis of the customer")
69
+ product_overview: str = Field(default=None, max_length=256, description="analysis of the client's business")
70
+ usp: str = Field()
71
+ cohort_timeframe: int = Field(default=None, max_length=256, description="suitable cohort timeframe in days")
72
+ kpi_metrics: List[str] = Field(default=list, description="Ideal KPIs to be tracked")
73
+ assumptions: List[Dict[str, Any]] = Field(default=list, description="assumptions to test")
74
+
75
+
76
+
33
77
  class TaskOutput(BaseModel):
34
78
  """
35
79
  Store the final output of the task in TaskOutput class.
36
80
  Depending on the task output format, use `raw`, `pydantic`, `json_dict` accordingly.
37
81
  """
38
82
 
39
- class AgentOutput(BaseModel):
40
- """
41
- Keep adding agents' learning and recommendation and store it in `pydantic` field of `TaskOutput` class.
42
- Since the TaskOutput class has `agent` field, we don't add any info on the agent that handled the task.
43
- """
44
-
45
- customer_id: str = Field(
46
- default=None, max_length=126, description="customer uuid"
47
- )
48
- customer_analysis: str = Field(
49
- default=None, max_length=256, description="analysis of the customer"
50
- )
51
- business_overview: str = Field(
52
- default=None,
53
- max_length=256,
54
- description="analysis of the client's business",
55
- )
56
- cohort_timeframe: int = Field(
57
- default=None,
58
- max_length=256,
59
- description="Suitable cohort timeframe in days",
60
- )
61
- kpi_metrics: List[str] = Field(
62
- default=list, description="Ideal KPIs to be tracked"
63
- )
64
- assumptions: List[Dict[str, Any]] = Field(
65
- default=list, description="assumptions to test"
66
- )
67
-
68
- task_id: UUID4 = Field(
69
- default_factory=uuid.uuid4, frozen=True, description="store Task ID"
70
- )
83
+ task_id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True, description="store Task ID")
71
84
  raw: str = Field(default="", description="Raw output of the task")
72
- pydantic: Optional[BaseModel | AgentOutput] = Field(
73
- default=None, description="Pydantic output of task"
74
- )
75
- json_dict: Optional[Dict[str, Any]] = Field(
76
- default=None, description="JSON dictionary of task"
77
- )
85
+ json_dict: Union[Dict[str, Any]] = Field(default=None, description="`raw` converted to dictionary")
86
+ pydantic: Optional[Any] = Field(default=None, description="`raw` converted to the abs. pydantic model")
78
87
 
79
88
  def __str__(self) -> str:
80
- return (
81
- str(self.pydantic)
82
- if self.pydantic
83
- else str(self.json_dict) if self.json_dict else self.raw
84
- )
89
+ return str(self.pydantic) if self.pydantic else str(self.json_dict) if self.json_dict else self.raw
85
90
 
86
91
  @property
87
92
  def json(self) -> Optional[str]:
@@ -95,16 +100,31 @@ class TaskOutput(BaseModel):
95
100
  )
96
101
  return json.dumps(self.json_dict)
97
102
 
103
+
98
104
  def to_dict(self) -> Dict[str, Any]:
99
- """Convert json_output and pydantic_output to a dictionary."""
105
+ """
106
+ Convert pydantic / raw output into dict and return the dict.
107
+ When we only have `raw` output, return `{ output: raw }` to avoid an error
108
+ """
109
+
100
110
  output_dict = {}
101
111
  if self.json_dict:
102
112
  output_dict.update(self.json_dict)
103
113
  elif self.pydantic:
104
114
  output_dict.update(self.pydantic.model_dump())
115
+ else:
116
+ output_dict.upate({ "output": self.raw })
105
117
  return output_dict
106
118
 
107
119
 
120
+ def context_prompting(self) -> str:
121
+ """
122
+ When the task is called as context, return its output in concise string to add it to the prompt
123
+ """
124
+ return json.dumps(self.json_dict) if self.json_dict else self.raw[0: 127]
125
+
126
+
127
+
108
128
  class Task(BaseModel):
109
129
  """
110
130
  Task to be executed by the agent or the team.
@@ -114,51 +134,29 @@ class Task(BaseModel):
114
134
 
115
135
  __hash__ = object.__hash__
116
136
 
117
- id: UUID4 = Field(
118
- default_factory=uuid.uuid4,
119
- frozen=True,
120
- description="unique identifier for the object, not set by user",
121
- )
137
+ id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True, description="unique identifier for the object, not set by user")
122
138
  name: Optional[str] = Field(default=None)
123
139
  description: str = Field(description="Description of the actual task")
124
140
  _original_description: str = PrivateAttr(default=None)
125
141
 
126
142
  # output
127
- expected_output_raw: bool = Field(default=False)
128
143
  expected_output_json: bool = Field(default=True)
129
144
  expected_output_pydantic: bool = Field(default=False)
130
- output_field_list: Optional[List[ResponseField]] = Field(
131
- default=[
132
- ResponseField(title="output", type="str", required=True),
133
- ]
134
- )
135
- output: Optional[TaskOutput] = Field(
136
- default=None, description="store the final task output in TaskOutput class"
145
+ output_field_list: List[ResponseField] = Field(
146
+ default=[ResponseField(title="output", type=str, required=False)],
147
+ description="provide output key and data type. this will be cascaded to the agent via task.prompt()"
137
148
  )
149
+ output: Optional[TaskOutput] = Field(default=None, description="store the final task output in TaskOutput class")
138
150
 
139
151
  # task setup
140
- context: Optional[List["Task"]] = Field(
141
- default=None, description="other tasks whose outputs should be used as context"
142
- )
143
- tools_called: Optional[List[ToolCalled]] = Field(
144
- default_factory=list, description="tools that the agent can use for this task"
145
- )
146
- take_tool_res_as_final: bool = Field(
147
- default=False,
148
- description="when set True, tools res will be stored in the `TaskOutput`",
149
- )
152
+ context: Optional[List["Task"]] = Field(default=None, description="other tasks whose outputs should be used as context")
153
+ tools_called: Optional[List[ToolCalled]] = Field(default_factory=list, description="tools that the agent can use for this task")
154
+ take_tool_res_as_final: bool = Field(default=False,description="when set True, tools res will be stored in the `TaskOutput`")
150
155
 
151
156
  prompt_context: Optional[str] = None
152
- async_execution: bool = Field(
153
- default=False,
154
- description="whether the task should be executed asynchronously or not",
155
- )
156
- config: Optional[Dict[str, Any]] = Field(
157
- default=None, description="configuration for the agent"
158
- )
159
- callback: Optional[Any] = Field(
160
- default=None, description="callback to be executed after the task is completed."
161
- )
157
+ async_execution: bool = Field(default=False,description="whether the task should be executed asynchronously or not")
158
+ config: Optional[Dict[str, Any]] = Field(default=None, description="configuration for the agent")
159
+ callback: Optional[Any] = Field(default=None, description="callback to be executed after the task is completed.")
162
160
 
163
161
  # recording
164
162
  processed_by_agents: Set[str] = Field(default_factory=set)
@@ -166,33 +164,38 @@ class Task(BaseModel):
166
164
  tools_errors: int = 0
167
165
  delegations: int = 0
168
166
 
167
+
169
168
  @property
170
- def output_prompt(self):
169
+ def output_prompt(self) -> str:
171
170
  """
172
171
  Draft prompts on the output format by converting `output_field_list` to dictionary.
173
172
  """
174
173
 
175
- output_prompt, output_dict = "", dict()
174
+ output_prompt, output_formats_to_follow = "", dict()
176
175
  for item in self.output_field_list:
177
- output_dict[item.title] = f"your answer in {item.type}"
176
+ output_formats_to_follow[item.title] = f"<Return your answer in {item.type.__name__}>"
178
177
 
179
178
  output_prompt = f"""
180
- The output formats include the following format:
181
- {output_dict}
179
+ Your outputs MUST adhere to the following format and should NOT include any irrelevant elements:
180
+ {output_formats_to_follow}
182
181
  """
183
182
  return output_prompt
184
183
 
184
+
185
185
  @property
186
186
  def expected_output_formats(self) -> List[TaskOutputFormat]:
187
- outputs = []
187
+ """
188
+ Return output formats in list with the ENUM item.
189
+ `TaskOutputFormat.RAW` is set as default.
190
+ """
191
+ outputs = [TaskOutputFormat.RAW,]
188
192
  if self.expected_output_json:
189
193
  outputs.append(TaskOutputFormat.JSON)
190
194
  if self.expected_output_pydantic:
191
195
  outputs.append(TaskOutputFormat.PYDANTIC)
192
- if self.expected_output_raw:
193
- outputs.append(TaskOutputFormat.RAW)
194
196
  return outputs
195
197
 
198
+
196
199
  @property
197
200
  def key(self) -> str:
198
201
  output_format = (
@@ -207,6 +210,7 @@ class Task(BaseModel):
207
210
  source = [self.description, output_format]
208
211
  return md5("|".join(source).encode(), usedforsecurity=False).hexdigest()
209
212
 
213
+
210
214
  @property
211
215
  def summary(self) -> str:
212
216
  return f"""
@@ -216,32 +220,30 @@ class Task(BaseModel):
216
220
  "task_tools": {", ".join([tool_called.tool.name for tool_called in self.tools_called])}
217
221
  """
218
222
 
223
+
219
224
  # validators
220
225
  @model_validator(mode="before")
221
226
  @classmethod
222
227
  def process_model_config(cls, values: Dict[str, Any]):
223
228
  return process_config(values_to_update=values, model_class=cls)
224
229
 
230
+
225
231
  @field_validator("id", mode="before")
226
232
  @classmethod
227
233
  def _deny_user_set_id(cls, v: Optional[UUID4]) -> None:
228
234
  if v:
229
- raise PydanticCustomError(
230
- "may_not_set_field", "This field is not to be set by the user.", {}
231
- )
235
+ raise PydanticCustomError("may_not_set_field", "This field is not to be set by the user.", {})
236
+
232
237
 
233
238
  @model_validator(mode="after")
234
239
  def validate_required_fields(self):
235
- required_fields = [
236
- "description",
237
- ]
240
+ required_fields = ["description",]
238
241
  for field in required_fields:
239
242
  if getattr(self, field) is None:
240
- raise ValueError(
241
- f"{field} must be provided either directly or through config"
242
- )
243
+ raise ValueError( f"{field} must be provided either directly or through config")
243
244
  return self
244
245
 
246
+
245
247
  @model_validator(mode="after")
246
248
  def set_attributes_based_on_config(self) -> "Task":
247
249
  """
@@ -253,15 +255,14 @@ class Task(BaseModel):
253
255
  setattr(self, key, value)
254
256
  return self
255
257
 
256
- @model_validator(mode="after")
257
- def validate_output_format(self):
258
- if (
259
- self.expected_output_json == False
260
- and self.expected_output_pydantic == False
261
- and self.expeceted_output_raw == False
262
- ):
263
- raise PydanticCustomError("Need to choose at least one output format.")
264
- return self
258
+
259
+ ## comment out as we set raw as the default TaskOutputFormat
260
+ # @model_validator(mode="after")
261
+ # def validate_output_format(self):
262
+ # if self.expected_output_json == False and self.expected_output_pydantic == False:
263
+ # raise PydanticCustomError("Need to choose at least one output format.")
264
+ # return self
265
+
265
266
 
266
267
  @model_validator(mode="after")
267
268
  def backup_description(self):
@@ -269,57 +270,84 @@ class Task(BaseModel):
269
270
  self._original_description = self.description
270
271
  return self
271
272
 
272
- def prompt(self, customer=str | None, product_overview=str | None) -> str:
273
+
274
+ def prompt(self, customer: str = None, product_overview: str = None) -> str:
273
275
  """
274
- Return the prompt of the task.
276
+ Format the task prompt and cascade it to the agent.
277
+ When the task has context, add context prompting of all the tasks in the context.
278
+ When we have cusotmer/product info, add them to the prompt.
275
279
  """
276
280
 
277
- task_slices = [
278
- self.description,
279
- f"Customer overview: {customer}",
280
- f"Product overview: {product_overview}",
281
- f"Follow the output formats decribled below. Your response should NOT contain any other element from the following formats.: {self.output_prompt}",
282
- ]
281
+ task_slices = [self.description, f"{self.output_prompt}"]
282
+
283
+ if self.context:
284
+ context_outputs = "\n".join([task.output.context_prompting() if hasattr(task, "output") else "" for task in self.context])
285
+ task_slices.insert(1, f"Take the following context into consideration: {context_outputs}")
286
+
287
+ if customer:
288
+ task_slices.insert(1, f"customer overview: {customer}")
289
+
290
+ if product_overview:
291
+ task_slices.insert(1, f"Product overview: {product_overview}")
292
+
283
293
  return "\n".join(task_slices)
284
294
 
285
- def _export_output(
286
- self, result: Any
287
- ) -> Tuple[Optional[BaseModel], Optional[Dict[str, Any]]]:
288
- output_pydantic: Optional[BaseModel] = None
289
- output_json: Optional[Dict[str, Any]] = None
290
- dict_output = None
291
295
 
292
- if isinstance(result, str):
296
+ def create_json_output(self, raw_result: Any) -> Optional[Dict[str, Any]]:
297
+ """
298
+ Create json (dict) output from the raw result.
299
+ """
300
+
301
+ output_json_dict: Optional[Dict[str, Any]] = None
302
+
303
+ if isinstance(raw_result, BaseModel):
304
+ output_json_dict = raw_result.model_dump()
305
+
306
+ elif isinstance(raw_result, dict):
307
+ output_json_dict = raw_result
308
+
309
+ elif isinstance(raw_result, str):
293
310
  try:
294
- dict_output = json.loads(result)
311
+ output_json_dict = json.loads(raw_result)
295
312
  except json.JSONDecodeError:
296
313
  try:
297
- dict_output = eval(result)
314
+ output_json_dict = eval(raw_result)
298
315
  except:
299
316
  try:
300
317
  import ast
301
-
302
- dict_output = ast.literal_eval(result)
318
+ output_json_dict = ast.literal_eval(raw_result)
303
319
  except:
304
- dict_output = None
320
+ output_json_dict = { "output": raw_result }
305
321
 
306
- if self.expected_output_json:
307
- if isinstance(result, dict):
308
- output_json = result
309
- elif isinstance(result, BaseModel):
310
- output_json = result.model_dump()
311
- else:
312
- output_json = dict_output
322
+ return output_json_dict
313
323
 
314
- if self.expected_output_pydantic:
315
- if isinstance(result, BaseModel):
316
- output_pydantic = result
317
- elif isinstance(result, dict):
318
- output_json = result
319
- else:
320
- output_pydantic = None
321
324
 
322
- return output_json, output_pydantic
325
+
326
+ def create_pydantic_output(self, output_json_dict: Dict[str, Any], raw_result: Any = None) -> Optional[Any]:
327
+ """
328
+ Create pydantic output from the `raw` result.
329
+ """
330
+
331
+ output_pydantic = None
332
+ if isinstance(raw_result, BaseModel):
333
+ output_pydantic = raw_result
334
+
335
+ elif hasattr(output_json_dict, "output"):
336
+ output_pydantic = create_model("PydanticTaskOutput", output=output_json_dict["output"], __base__=BaseModel)
337
+
338
+ else:
339
+ output_pydantic = create_model("PydanticTaskOutput", __base__=BaseModel)
340
+ try:
341
+ for item in self.output_field_list:
342
+ value = output_json_dict[item.title] if hasattr(output_json_dict, item.title) else None
343
+ if value and type(value) is not item.type:
344
+ value = item._convert(value)
345
+ setattr(output_pydantic, item.title, value)
346
+ except:
347
+ setattr(output_pydantic, "output", output_json_dict)
348
+
349
+ return output_pydantic
350
+
323
351
 
324
352
  def _get_output_format(self) -> TaskOutputFormat:
325
353
  if self.output_json == True:
@@ -328,6 +356,7 @@ class Task(BaseModel):
328
356
  return TaskOutputFormat.PYDANTIC
329
357
  return TaskOutputFormat.RAW
330
358
 
359
+
331
360
  def interpolate_inputs(self, inputs: Dict[str, Any]) -> None:
332
361
  """
333
362
  Interpolate inputs into the task description and expected output.
@@ -336,13 +365,22 @@ class Task(BaseModel):
336
365
  self.description = self._original_description.format(**inputs)
337
366
  # self.expected_output = self._original_expected_output.format(**inputs)
338
367
 
368
+
339
369
  # task execution
340
370
  def execute_sync(self, agent, context: Optional[str] = None) -> TaskOutput:
341
371
  """
342
372
  Execute the task synchronously.
373
+ When the task has context, make sure we have executed all the tasks in the context first.
343
374
  """
375
+
376
+ if self.context:
377
+ for task in self.context:
378
+ if task.output is None:
379
+ task._execute_core(agent, context)
380
+
344
381
  return self._execute_core(agent, context)
345
382
 
383
+
346
384
  def execute_async(self, agent, context: Optional[str] = None) -> Future[TaskOutput]:
347
385
  """
348
386
  Execute the task asynchronously.
@@ -356,26 +394,27 @@ class Task(BaseModel):
356
394
  ).start()
357
395
  return future
358
396
 
359
- def _execute_task_async(
360
- self, agent, context: Optional[str], future: Future[TaskOutput]
361
- ) -> None:
397
+
398
+ def _execute_task_async(self, agent, context: Optional[str], future: Future[TaskOutput]) -> None:
362
399
  """Execute the task asynchronously with context handling."""
363
400
  result = self._execute_core(agent, context)
364
401
  future.set_result(result)
365
402
 
403
+
366
404
  def _execute_core(self, agent, context: Optional[str]) -> TaskOutput:
367
405
  """
368
406
  Run the core execution logic of the task.
369
407
  """
370
408
 
371
409
  self.prompt_context = context
372
- result = agent.execute_task(task=self, context=context)
373
- output_json, output_pydantic = self._export_output(result)
410
+ output_raw = agent.execute_task(task=self, context=context)
411
+ output_json_dict = self.create_json_output(raw_result=output_raw)
412
+ output_pydantic = self.create_pydantic_output(output_json_dict=output_json_dict)
374
413
  task_output = TaskOutput(
375
414
  task_id=self.id,
376
- raw=result,
415
+ raw=output_raw,
377
416
  pydantic=output_pydantic,
378
- json_dict=output_json,
417
+ json_dict=output_json_dict
379
418
  )
380
419
  self.output = task_output
381
420
  self.processed_by_agents.add(agent.role)
@@ -396,7 +435,6 @@ class Task(BaseModel):
396
435
  # else pydantic_output.model_dump_json() if pydantic_output else result
397
436
  # )
398
437
  # self._save_file(content)
399
-
400
438
  return task_output
401
439
 
402
440