versionhq 1.1.6.4__py3-none-any.whl → 1.1.7.0__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
@@ -24,11 +18,36 @@ class ResponseField(BaseModel):
24
18
  """
25
19
  Field class to use in the response schema for the JSON response.
26
20
  """
27
-
28
21
  title: str = Field(default=None)
29
- type: str = Field(default=None)
22
+ type: Type = Field(default=str)
30
23
  required: bool = Field(default=True)
31
24
 
25
+ def _annotate(self, value: Any) -> Annotated:
26
+ """
27
+ Address `create_model`
28
+ """
29
+ return Annotated[self.type, value] if isinstance(value, self.type) else Annotated[str, str(value)]
30
+
31
+ def create_pydantic_model(self, result: Dict, base_model: Union[BaseModel | Any]) -> Any:
32
+ for k, v in result.items():
33
+ if k is not self.title or type(v) is not self.type:
34
+ pass
35
+ setattr(base_model, k, v)
36
+ return base_model
37
+
38
+
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
+ customer_id: str = Field(default=None, max_length=126, description="customer uuid")
45
+ customer_analysis: str = Field(default=None, max_length=256, description="analysis of the customer")
46
+ business_overview: str = Field(default=None,max_length=256,description="analysis of the client's business")
47
+ cohort_timeframe: int = Field(default=None,max_length=256,description="Suitable cohort timeframe in days")
48
+ kpi_metrics: List[str] = Field(default=list, description="Ideal KPIs to be tracked")
49
+ assumptions: List[Dict[str, Any]] = Field(default=list, description="assumptions to test")
50
+
32
51
 
33
52
  class TaskOutput(BaseModel):
34
53
  """
@@ -36,52 +55,13 @@ class TaskOutput(BaseModel):
36
55
  Depending on the task output format, use `raw`, `pydantic`, `json_dict` accordingly.
37
56
  """
38
57
 
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
- )
58
+ task_id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True, description="store Task ID")
71
59
  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
- )
60
+ pydantic: Optional[Any] = Field(default=None, description="`raw` converted to the abs. pydantic model")
61
+ json_dict: Union[Dict[str, Any]] = Field(default=None, description="`raw` converted to dictionary")
78
62
 
79
63
  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
- )
64
+ return str(self.pydantic) if self.pydantic else str(self.json_dict) if self.json_dict else self.raw
85
65
 
86
66
  @property
87
67
  def json(self) -> Optional[str]:
@@ -114,51 +94,26 @@ class Task(BaseModel):
114
94
 
115
95
  __hash__ = object.__hash__
116
96
 
117
- id: UUID4 = Field(
118
- default_factory=uuid.uuid4,
119
- frozen=True,
120
- description="unique identifier for the object, not set by user",
121
- )
97
+ id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True, description="unique identifier for the object, not set by user")
122
98
  name: Optional[str] = Field(default=None)
123
99
  description: str = Field(description="Description of the actual task")
124
100
  _original_description: str = PrivateAttr(default=None)
125
101
 
126
102
  # output
127
- expected_output_raw: bool = Field(default=False)
128
103
  expected_output_json: bool = Field(default=True)
129
104
  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"
137
- )
105
+ output_field_list: Optional[List[ResponseField]] = Field(default=[ResponseField(title="output")])
106
+ output: Optional[TaskOutput] = Field(default=None, description="store the final task output in TaskOutput class")
138
107
 
139
108
  # 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
- )
109
+ context: Optional[List["Task"]] = Field(default=None, description="other tasks whose outputs should be used as context")
110
+ tools_called: Optional[List[ToolCalled]] = Field(default_factory=list, description="tools that the agent can use for this task")
111
+ take_tool_res_as_final: bool = Field(default=False,description="when set True, tools res will be stored in the `TaskOutput`")
150
112
 
151
113
  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
- )
114
+ async_execution: bool = Field(default=False,description="whether the task should be executed asynchronously or not")
115
+ config: Optional[Dict[str, Any]] = Field(default=None, description="configuration for the agent")
116
+ callback: Optional[Any] = Field(default=None, description="callback to be executed after the task is completed.")
162
117
 
163
118
  # recording
164
119
  processed_by_agents: Set[str] = Field(default_factory=set)
@@ -166,6 +121,7 @@ class Task(BaseModel):
166
121
  tools_errors: int = 0
167
122
  delegations: int = 0
168
123
 
124
+
169
125
  @property
170
126
  def output_prompt(self):
171
127
  """
@@ -174,25 +130,29 @@ class Task(BaseModel):
174
130
 
175
131
  output_prompt, output_dict = "", dict()
176
132
  for item in self.output_field_list:
177
- output_dict[item.title] = f"your answer in {item.type}"
133
+ output_dict[item.title] = f"<Return your answer in {item.type.__name__}>"
178
134
 
179
135
  output_prompt = f"""
180
- The output formats include the following format:
136
+ Your outputs STRICTLY follow the following format and should NOT contain any other irrevant elements that not specified in the following format:
181
137
  {output_dict}
182
138
  """
183
139
  return output_prompt
184
140
 
141
+
185
142
  @property
186
143
  def expected_output_formats(self) -> List[TaskOutputFormat]:
187
- outputs = []
144
+ """
145
+ Return output formats in list with the ENUM item.
146
+ `TaskOutputFormat.RAW` is set as default.
147
+ """
148
+ outputs = [TaskOutputFormat.RAW,]
188
149
  if self.expected_output_json:
189
150
  outputs.append(TaskOutputFormat.JSON)
190
151
  if self.expected_output_pydantic:
191
152
  outputs.append(TaskOutputFormat.PYDANTIC)
192
- if self.expected_output_raw:
193
- outputs.append(TaskOutputFormat.RAW)
194
153
  return outputs
195
154
 
155
+
196
156
  @property
197
157
  def key(self) -> str:
198
158
  output_format = (
@@ -207,6 +167,7 @@ class Task(BaseModel):
207
167
  source = [self.description, output_format]
208
168
  return md5("|".join(source).encode(), usedforsecurity=False).hexdigest()
209
169
 
170
+
210
171
  @property
211
172
  def summary(self) -> str:
212
173
  return f"""
@@ -216,32 +177,30 @@ class Task(BaseModel):
216
177
  "task_tools": {", ".join([tool_called.tool.name for tool_called in self.tools_called])}
217
178
  """
218
179
 
180
+
219
181
  # validators
220
182
  @model_validator(mode="before")
221
183
  @classmethod
222
184
  def process_model_config(cls, values: Dict[str, Any]):
223
185
  return process_config(values_to_update=values, model_class=cls)
224
186
 
187
+
225
188
  @field_validator("id", mode="before")
226
189
  @classmethod
227
190
  def _deny_user_set_id(cls, v: Optional[UUID4]) -> None:
228
191
  if v:
229
- raise PydanticCustomError(
230
- "may_not_set_field", "This field is not to be set by the user.", {}
231
- )
192
+ raise PydanticCustomError("may_not_set_field", "This field is not to be set by the user.", {})
193
+
232
194
 
233
195
  @model_validator(mode="after")
234
196
  def validate_required_fields(self):
235
- required_fields = [
236
- "description",
237
- ]
197
+ required_fields = ["description",]
238
198
  for field in required_fields:
239
199
  if getattr(self, field) is None:
240
- raise ValueError(
241
- f"{field} must be provided either directly or through config"
242
- )
200
+ raise ValueError( f"{field} must be provided either directly or through config")
243
201
  return self
244
202
 
203
+
245
204
  @model_validator(mode="after")
246
205
  def set_attributes_based_on_config(self) -> "Task":
247
206
  """
@@ -253,15 +212,14 @@ class Task(BaseModel):
253
212
  setattr(self, key, value)
254
213
  return self
255
214
 
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
215
+
216
+ ## comment out as we set raw as the default TaskOutputFormat
217
+ # @model_validator(mode="after")
218
+ # def validate_output_format(self):
219
+ # if self.expected_output_json == False and self.expected_output_pydantic == False:
220
+ # raise PydanticCustomError("Need to choose at least one output format.")
221
+ # return self
222
+
265
223
 
266
224
  @model_validator(mode="after")
267
225
  def backup_description(self):
@@ -269,57 +227,69 @@ class Task(BaseModel):
269
227
  self._original_description = self.description
270
228
  return self
271
229
 
230
+
272
231
  def prompt(self, customer=str | None, product_overview=str | None) -> str:
273
232
  """
274
- Return the prompt of the task.
233
+ Format the task prompt.
275
234
  """
276
-
277
235
  task_slices = [
278
236
  self.description,
279
237
  f"Customer overview: {customer}",
280
238
  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}",
239
+ f"{self.output_prompt}",
282
240
  ]
283
241
  return "\n".join(task_slices)
284
242
 
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
243
 
292
- if isinstance(result, str):
244
+ def create_json_output(self, raw_result: Any) -> Optional[Dict[str, Any]]:
245
+ """
246
+ Create json (dict) output from the raw result.
247
+ """
248
+
249
+ output_json_dict: Optional[Dict[str, Any]] = None
250
+
251
+ if isinstance(raw_result, BaseModel):
252
+ output_json_dict = raw_result.model_dump()
253
+
254
+ elif isinstance(raw_result, dict):
255
+ output_json_dict = raw_result
256
+
257
+ elif isinstance(raw_result, str):
293
258
  try:
294
- dict_output = json.loads(result)
259
+ output_json_dict = json.loads(raw_result)
295
260
  except json.JSONDecodeError:
296
261
  try:
297
- dict_output = eval(result)
262
+ output_json_dict = eval(raw_result)
298
263
  except:
299
264
  try:
300
265
  import ast
301
-
302
- dict_output = ast.literal_eval(result)
266
+ output_json_dict = ast.literal_eval(raw_result)
303
267
  except:
304
- dict_output = None
268
+ output_json_dict = { "output": raw_result }
305
269
 
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
270
+ return output_json_dict
313
271
 
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
272
 
322
- return output_json, output_pydantic
273
+
274
+ def create_pydantic_output(self, output_json_dict: Dict[str, Any], raw_result: Any = None) -> Optional[Any]:
275
+ """
276
+ Create pydantic output from the raw result.
277
+ """
278
+
279
+ output_pydantic = None #! REFINEME
280
+ if isinstance(raw_result, BaseModel):
281
+ output_pydantic = raw_result
282
+
283
+ elif hasattr(output_json_dict, "output"):
284
+ output_pydantic = create_model("PydanticTaskOutput", output=output_json_dict["output"], __base__=BaseModel)
285
+
286
+ else:
287
+ output_pydantic = create_model("PydanticTaskOutput", __base__=BaseModel)
288
+ for item in self.output_field_list:
289
+ item.create_pydantic_model(result=output_json_dict, base_model=output_pydantic)
290
+
291
+ return output_pydantic
292
+
323
293
 
324
294
  def _get_output_format(self) -> TaskOutputFormat:
325
295
  if self.output_json == True:
@@ -328,6 +298,7 @@ class Task(BaseModel):
328
298
  return TaskOutputFormat.PYDANTIC
329
299
  return TaskOutputFormat.RAW
330
300
 
301
+
331
302
  def interpolate_inputs(self, inputs: Dict[str, Any]) -> None:
332
303
  """
333
304
  Interpolate inputs into the task description and expected output.
@@ -343,6 +314,7 @@ class Task(BaseModel):
343
314
  """
344
315
  return self._execute_core(agent, context)
345
316
 
317
+
346
318
  def execute_async(self, agent, context: Optional[str] = None) -> Future[TaskOutput]:
347
319
  """
348
320
  Execute the task asynchronously.
@@ -356,26 +328,27 @@ class Task(BaseModel):
356
328
  ).start()
357
329
  return future
358
330
 
359
- def _execute_task_async(
360
- self, agent, context: Optional[str], future: Future[TaskOutput]
361
- ) -> None:
331
+
332
+ def _execute_task_async(self, agent, context: Optional[str], future: Future[TaskOutput]) -> None:
362
333
  """Execute the task asynchronously with context handling."""
363
334
  result = self._execute_core(agent, context)
364
335
  future.set_result(result)
365
336
 
337
+
366
338
  def _execute_core(self, agent, context: Optional[str]) -> TaskOutput:
367
339
  """
368
340
  Run the core execution logic of the task.
369
341
  """
370
342
 
371
343
  self.prompt_context = context
372
- result = agent.execute_task(task=self, context=context)
373
- output_json, output_pydantic = self._export_output(result)
344
+ raw_result = agent.execute_task(task=self, context=context)
345
+ output_json_dict = self.create_json_output(raw_result=raw_result)
346
+ output_pydantic = self.create_pydantic_output(output_json_dict=output_json_dict)
374
347
  task_output = TaskOutput(
375
348
  task_id=self.id,
376
- raw=result,
349
+ raw=raw_result,
377
350
  pydantic=output_pydantic,
378
- json_dict=output_json,
351
+ json_dict=output_json_dict
379
352
  )
380
353
  self.output = task_output
381
354
  self.processed_by_agents.add(agent.role)
@@ -396,7 +369,6 @@ class Task(BaseModel):
396
369
  # else pydantic_output.model_dump_json() if pydantic_output else result
397
370
  # )
398
371
  # self._save_file(content)
399
-
400
372
  return task_output
401
373
 
402
374