agently 4.0.6.5__py3-none-any.whl → 4.0.6.10__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.
@@ -22,12 +22,13 @@ ContentKindTuple: TypeAlias = Literal["all", "delta", "original"]
22
22
  ContentKindStreaming: TypeAlias = Literal["instant", "streaming_parse"]
23
23
 
24
24
  from agently.core import Prompt, ExtensionHandlers
25
- from agently.utils import Settings, FunctionShifter, DataFormatter
25
+ from agently.utils import Settings, FunctionShifter, DataFormatter, DataLocator
26
26
 
27
27
  if TYPE_CHECKING:
28
28
  from agently.core import PluginManager
29
29
  from agently.types.data import AgentlyModelResponseMessage, PromptStandardSlot, StreamingData, SerializableValue
30
30
  from agently.types.plugins import ModelRequester, ResponseParser
31
+ from pydantic import BaseModel
31
32
 
32
33
 
33
34
  class ModelResponseResult:
@@ -39,6 +40,7 @@ class ModelResponseResult:
39
40
  response_generator: AsyncGenerator["AgentlyModelResponseMessage", None],
40
41
  plugin_manager: "PluginManager",
41
42
  settings: Settings,
43
+ extension_handlers: ExtensionHandlers,
42
44
  ):
43
45
  self.agent_name = agent_name
44
46
  self.plugin_manager = plugin_manager
@@ -50,19 +52,145 @@ class ModelResponseResult:
50
52
  str(self.settings["plugins.ResponseParser.activate"]),
51
53
  ),
52
54
  )
53
- _response_parser = ResponseParser(agent_name, response_id, prompt, response_generator, self.settings)
55
+ self._response_id = response_id
56
+ self._extension_handlers = extension_handlers
57
+ self._response_parser = ResponseParser(agent_name, response_id, prompt, response_generator, self.settings)
54
58
  self.prompt = prompt
55
- self.full_result_data = _response_parser.full_result_data
56
- self.get_meta = _response_parser.get_meta
57
- self.async_get_meta = _response_parser.async_get_meta
58
- self.get_text = _response_parser.get_text
59
- self.async_get_text = _response_parser.async_get_text
60
- self.get_data = _response_parser.get_data
61
- self.async_get_data = _response_parser.async_get_data
62
- self.get_data_object = _response_parser.get_data_object
63
- self.async_get_data_object = _response_parser.async_get_data_object
64
- self.get_generator = _response_parser.get_generator
65
- self.get_async_generator = _response_parser.get_async_generator
59
+ self.full_result_data = self._response_parser.full_result_data
60
+ self.get_meta = self._response_parser.get_meta
61
+ self.async_get_meta = self._response_parser.async_get_meta
62
+ self.get_text = self._response_parser.get_text
63
+ self.async_get_text = self._response_parser.async_get_text
64
+ # self.get_data = self._response_parser.get_data
65
+ # self.async_get_data = self._response_parser.async_get_data
66
+ # self.get_data_object = self._response_parser.get_data_object
67
+ # self.async_get_data_object = self._response_parser.async_get_data_object
68
+ self.get_data = FunctionShifter.syncify(self.async_get_data)
69
+ self.get_data_object = FunctionShifter.syncify(self.async_get_data_object)
70
+ self.get_generator = self._response_parser.get_generator
71
+ self.get_async_generator = self._response_parser.get_async_generator
72
+
73
+ @overload
74
+ async def async_get_data(
75
+ self,
76
+ *,
77
+ type: Literal['parsed'],
78
+ ensure_keys: list[str],
79
+ key_style: Literal["dot", "slash"] = "dot",
80
+ max_retries: int = 3,
81
+ raise_ensure_failure: bool = True,
82
+ _retry_count: int = 0,
83
+ ) -> dict[str, Any]: ...
84
+
85
+ @overload
86
+ async def async_get_data(
87
+ self,
88
+ *,
89
+ type: Literal['original', 'parsed', 'all'] = "parsed",
90
+ ensure_keys: list[str] | None = None,
91
+ key_style: Literal["dot", "slash"] = "dot",
92
+ max_retries: int = 3,
93
+ raise_ensure_failure: bool = True,
94
+ _retry_count: int = 0,
95
+ ) -> Any: ...
96
+
97
+ async def async_get_data(
98
+ self,
99
+ *,
100
+ type: Literal['original', 'parsed', 'all'] = "parsed",
101
+ ensure_keys: list[str] | None = None,
102
+ key_style: Literal["dot", "slash"] = "dot",
103
+ max_retries: int = 3,
104
+ raise_ensure_failure: bool = True,
105
+ _retry_count: int = 0,
106
+ ) -> Any:
107
+ if type == "parsed" and ensure_keys:
108
+ try:
109
+ data = await self._response_parser.async_get_data(type=type)
110
+ for ensure_key in ensure_keys:
111
+ EMPTY = object()
112
+ if DataLocator.locate_path_in_dict(data, ensure_key, key_style, default=EMPTY) is EMPTY:
113
+ raise
114
+ return data
115
+ except:
116
+ from agently.base import async_system_message
117
+
118
+ await async_system_message(
119
+ "MODEL_REQUEST",
120
+ {
121
+ "agent_name": self.agent_name,
122
+ "response_id": self._response_id,
123
+ "content": {
124
+ "stage": "No Target Data in Response, Preparing Retry",
125
+ "detail": f"\n[Response]: { await self.async_get_text() }\n"
126
+ f"[Retried Times]: { _retry_count }",
127
+ },
128
+ },
129
+ self.settings,
130
+ )
131
+
132
+ if _retry_count < max_retries:
133
+ return await ModelResponse(
134
+ self.agent_name,
135
+ self.plugin_manager,
136
+ self.settings,
137
+ self.prompt,
138
+ self._extension_handlers,
139
+ ).result.async_get_data(
140
+ type=type,
141
+ ensure_keys=ensure_keys,
142
+ key_style=key_style,
143
+ max_retries=max_retries,
144
+ raise_ensure_failure=raise_ensure_failure,
145
+ _retry_count=_retry_count + 1,
146
+ )
147
+ else:
148
+ if raise_ensure_failure:
149
+ raise ValueError(
150
+ f"Can not generate ensure keys { ensure_keys } within { max_retries } retires."
151
+ )
152
+ else:
153
+ return await self._response_parser.async_get_data(type=type)
154
+ return await self._response_parser.async_get_data(type=type)
155
+
156
+ @overload
157
+ async def async_get_data_object(
158
+ self,
159
+ *,
160
+ ensure_keys: list[str],
161
+ key_style: Literal["dot", "slash"] = "dot",
162
+ max_retries: int = 3,
163
+ raise_ensure_failure: bool = True,
164
+ ) -> "BaseModel": ...
165
+
166
+ @overload
167
+ async def async_get_data_object(
168
+ self,
169
+ *,
170
+ ensure_keys: None,
171
+ key_style: Literal["dot", "slash"] = "dot",
172
+ max_retries: int = 3,
173
+ raise_ensure_failure: bool = True,
174
+ ) -> "BaseModel | None": ...
175
+
176
+ async def async_get_data_object(
177
+ self,
178
+ *,
179
+ ensure_keys: list[str] | None = None,
180
+ key_style: Literal["dot", "slash"] = "dot",
181
+ max_retries: int = 3,
182
+ raise_ensure_failure: bool = True,
183
+ ):
184
+ if ensure_keys:
185
+ await self.async_get_data(
186
+ ensure_keys=ensure_keys,
187
+ key_style=key_style,
188
+ max_retries=max_retries,
189
+ _retry_count=0,
190
+ raise_ensure_failure=raise_ensure_failure,
191
+ )
192
+ return await self._response_parser.async_get_data_object()
193
+ return await self._response_parser.async_get_data_object()
66
194
 
67
195
 
68
196
  class ModelResponse:
@@ -98,8 +226,8 @@ class ModelResponse:
98
226
  self._get_response_generator(),
99
227
  self.plugin_manager,
100
228
  self.settings,
229
+ self.extension_handlers,
101
230
  )
102
- self.get_result = self.result
103
231
  self.get_meta = self.result.get_meta
104
232
  self.async_get_meta = self.result.async_get_meta
105
233
  self.get_text = self.result.get_text
@@ -395,6 +523,15 @@ class ModelRequest:
395
523
  self.prompt.set("output", prompt, mappings)
396
524
  return self
397
525
 
526
+ def attachment(
527
+ self,
528
+ prompt: list[dict[str, Any]],
529
+ mappings: dict[str, Any] | None = None,
530
+ ):
531
+ self.prompt.set("attachment", prompt, mappings)
532
+ return self
533
+
534
+ # Response & Result
398
535
  def get_response(self):
399
536
  response = ModelResponse(
400
537
  self.agent_name,
@@ -406,7 +543,7 @@ class ModelRequest:
406
543
  self.prompt.clear()
407
544
  return response
408
545
 
409
- async def get_result(self):
546
+ def get_result(self):
410
547
  return self.get_response().result
411
548
 
412
549
  async def async_get_meta(self):
@@ -419,11 +556,35 @@ class ModelRequest:
419
556
  self,
420
557
  *,
421
558
  type: Literal['original', 'parsed', 'all'] = "parsed",
559
+ ensure_keys: list[str] | None = None,
560
+ key_style: Literal["dot", "slash"] = "dot",
561
+ max_retries: int = 3,
562
+ raise_ensure_failure: bool = True,
422
563
  ):
423
- return await self.get_response().async_get_data(type=type)
564
+ response = self.get_response()
565
+ return await response.async_get_data(
566
+ type=type,
567
+ ensure_keys=ensure_keys,
568
+ key_style=key_style,
569
+ max_retries=max_retries,
570
+ raise_ensure_failure=raise_ensure_failure,
571
+ )
424
572
 
425
- async def async_get_data_object(self):
426
- return await self.get_response().async_get_data_object()
573
+ async def async_get_data_object(
574
+ self,
575
+ *,
576
+ ensure_keys: list[str] | None = None,
577
+ key_style: Literal["dot", "slash"] = "dot",
578
+ max_retries: int = 3,
579
+ raise_ensure_failure: bool = True,
580
+ ):
581
+ response = self.get_response()
582
+ return await response.async_get_data_object(
583
+ ensure_keys=ensure_keys,
584
+ key_style=key_style,
585
+ max_retries=max_retries,
586
+ raise_ensure_failure=raise_ensure_failure,
587
+ )
427
588
 
428
589
  @overload
429
590
  def get_generator(
agently/core/Prompt.py CHANGED
@@ -13,6 +13,7 @@
13
13
  # limitations under the License.
14
14
 
15
15
  import re
16
+ from textwrap import dedent
16
17
  from typing import Any, Literal, Mapping, Sequence, TYPE_CHECKING, cast, overload, TypeVar
17
18
 
18
19
  from agently.utils import RuntimeData, Settings
@@ -98,6 +99,9 @@ class Prompt(RuntimeData):
98
99
  self.to_messages = self.prompt_generator.to_messages
99
100
  self.to_prompt_object = self.prompt_generator.to_prompt_object
100
101
  self.to_output_model = self.prompt_generator.to_output_model
102
+ self.to_serializable_prompt_data = self.prompt_generator.to_serializable_prompt_data
103
+ self.to_json_prompt = self.prompt_generator.to_json_prompt
104
+ self.to_yaml_prompt = self.prompt_generator.to_yaml_prompt
101
105
 
102
106
  def _substitute_placeholder(self, obj: T, variable_mappings: dict[str, Any]) -> T | Any:
103
107
  if not isinstance(variable_mappings, dict):
@@ -157,6 +161,8 @@ class Prompt(RuntimeData):
157
161
  value: Any,
158
162
  mappings: dict[str, Any] | None = None,
159
163
  ):
164
+ if isinstance(value, str):
165
+ value = dedent(value.strip())
160
166
  if mappings is not None:
161
167
  super().set(
162
168
  self._substitute_placeholder(key, mappings),
@@ -183,6 +189,8 @@ class Prompt(RuntimeData):
183
189
  value: Any,
184
190
  mappings: dict[str, Any] | None = None,
185
191
  ):
192
+ if isinstance(value, str):
193
+ value = dedent(value.strip())
186
194
  if mappings is not None:
187
195
  super().append(
188
196
  self._substitute_placeholder(key, mappings),
@@ -36,10 +36,10 @@ class TriggerFlowChunk:
36
36
 
37
37
  async def async_call(self, data: "TriggerFlowEventData"):
38
38
  result = await FunctionShifter.asyncify(self._handler)(data)
39
- await data.async_emit(self.trigger, result, layer_marks=data.layer_marks.copy())
39
+ await data.async_emit(self.trigger, result, _layer_marks=data._layer_marks.copy())
40
40
  return result
41
41
 
42
42
  def call(self, data: "TriggerFlowEventData"):
43
43
  result = FunctionShifter.syncify(self._handler)(data)
44
- data.emit(self.trigger, result, layer_marks=data.layer_marks.copy())
44
+ data.emit(self.trigger, result, _layer_marks=data._layer_marks.copy())
45
45
  return result
@@ -97,7 +97,7 @@ class TriggerFlowExecution:
97
97
  self,
98
98
  trigger_event: str,
99
99
  value: Any = None,
100
- layer_marks: list[str] | None = None,
100
+ _layer_marks: list[str] | None = None,
101
101
  *,
102
102
  trigger_type: Literal["event", "runtime_data", "flow_data"] = "event",
103
103
  ):
@@ -134,7 +134,7 @@ class TriggerFlowExecution:
134
134
  trigger_type=trigger_type,
135
135
  value=value,
136
136
  execution=self,
137
- layer_marks=layer_marks,
137
+ _layer_marks=_layer_marks,
138
138
  )
139
139
  )
140
140
  )
@@ -178,7 +178,7 @@ class TriggerFlowBaseProcess:
178
178
  if mode == "simple_or"
179
179
  else (data.trigger_type, data.trigger_event, data.value)
180
180
  ),
181
- layer_marks=data.layer_marks.copy(),
181
+ _layer_marks=data._layer_marks.copy(),
182
182
  )
183
183
  case "and":
184
184
  if data.trigger_type in values and data.trigger_event in values[trigger_type]: # type: ignore
@@ -191,7 +191,7 @@ class TriggerFlowBaseProcess:
191
191
  await data.async_emit(
192
192
  when_trigger,
193
193
  values,
194
- layer_marks=data.layer_marks.copy(),
194
+ _layer_marks=data._layer_marks.copy(),
195
195
  )
196
196
 
197
197
  for trigger_type, trigger_event_dict in values.items():
@@ -213,15 +213,21 @@ class TriggerFlowBaseProcess:
213
213
 
214
214
  def to(
215
215
  self,
216
- chunk: "TriggerFlowChunk | TriggerFlowHandler | str",
216
+ chunk: "TriggerFlowChunk | TriggerFlowHandler | str | tuple[str, TriggerFlowHandler]",
217
217
  side_branch: bool = False,
218
+ name: str | None = None,
218
219
  ):
219
220
  if isinstance(chunk, str):
220
221
  if chunk in self._blue_print.chunks:
221
222
  chunk = self._blue_print.chunks[chunk]
222
223
  else:
223
224
  raise NotImplementedError(f"Cannot find chunk named '{ chunk }'")
224
- chunk = TriggerFlowChunk(chunk) if callable(chunk) else chunk
225
+ elif isinstance(chunk, tuple):
226
+ chunk_name = chunk[0]
227
+ chunk_func = chunk[1]
228
+ chunk = TriggerFlowChunk(chunk_func, name=chunk_name)
229
+ else:
230
+ chunk = TriggerFlowChunk(chunk, name=name) if callable(chunk) else chunk
225
231
  self._blue_print.add_handler(
226
232
  self.trigger_type,
227
233
  self.trigger_event,
@@ -235,34 +241,51 @@ class TriggerFlowBaseProcess:
235
241
  **self._options,
236
242
  )
237
243
 
238
- def side_branch(self, chunk: "TriggerFlowChunk | TriggerFlowHandler"):
239
- return self.to(chunk, side_branch=True)
244
+ def side_branch(
245
+ self,
246
+ chunk: "TriggerFlowChunk | TriggerFlowHandler",
247
+ *,
248
+ name: str | None = None,
249
+ ):
250
+ return self.to(
251
+ chunk,
252
+ side_branch=True,
253
+ name=name,
254
+ )
240
255
 
241
256
  def batch(
242
257
  self,
243
- *chunks: "TriggerFlowChunk | TriggerFlowHandler",
258
+ *chunks: "TriggerFlowChunk | TriggerFlowHandler | tuple[str, TriggerFlowHandler]",
244
259
  side_branch: bool = False,
245
260
  ):
246
261
  batch_trigger = f"Batch-{ uuid.uuid4().hex }"
247
262
  results = {}
248
- chunks_to_wait = {}
263
+ triggers_to_wait = {}
264
+ trigger_to_chunk_name = {}
249
265
 
250
266
  async def wait_all_chunks(data: "TriggerFlowEventData"):
251
- if data.event in chunks_to_wait:
252
- results[data.event] = data.value
253
- chunks_to_wait[data.event] = True
254
- for done in chunks_to_wait.values():
267
+ if data.event in triggers_to_wait:
268
+ results[trigger_to_chunk_name[data.event]] = data.value
269
+ triggers_to_wait[data.event] = True
270
+ for done in triggers_to_wait.values():
255
271
  if done is False:
256
272
  return
257
273
  await data.async_emit(
258
274
  batch_trigger,
259
275
  results,
260
- layer_marks=data.layer_marks.copy(),
276
+ _layer_marks=data._layer_marks.copy(),
261
277
  )
262
278
 
263
279
  for chunk in chunks:
264
- chunk = TriggerFlowChunk(chunk) if callable(chunk) else chunk
265
- chunks_to_wait[chunk.name] = False
280
+ if isinstance(chunk, tuple):
281
+ chunk_name = chunk[0]
282
+ chunk_func = chunk[1]
283
+ chunk = TriggerFlowChunk(chunk_func, name=chunk_name)
284
+ else:
285
+ chunk = TriggerFlowChunk(chunk) if callable(chunk) else chunk
286
+ triggers_to_wait[chunk.trigger] = False
287
+ trigger_to_chunk_name[chunk.trigger] = chunk.name
288
+ results[chunk.name] = None
266
289
  self._blue_print.add_handler(
267
290
  self.trigger_type,
268
291
  self.trigger_event,
@@ -299,13 +322,13 @@ class TriggerFlowBaseProcess:
299
322
  await data.async_emit(
300
323
  collect_trigger,
301
324
  self._block_data.global_data.get(f"collections.{ collection_name}"),
302
- layer_marks=data.layer_marks.copy(),
325
+ _layer_marks=data._layer_marks.copy(),
303
326
  )
304
327
  elif mode == "filled_then_empty":
305
328
  await data.async_emit(
306
329
  collect_trigger,
307
330
  self._block_data.global_data.get(f"collections.{ collection_name}"),
308
- layer_marks=data.layer_marks.copy(),
331
+ _layer_marks=data._layer_marks.copy(),
309
332
  )
310
333
  del self._block_data.global_data[f"collections.{ collection_name}"]
311
334
 
@@ -49,7 +49,7 @@ class TriggerFlowForEachProcess(TriggerFlowBaseProcess):
49
49
  data.async_emit(
50
50
  send_item_trigger,
51
51
  item,
52
- data.layer_marks.copy(),
52
+ data._layer_marks.copy(),
53
53
  )
54
54
  )
55
55
  data.layer_out()
@@ -62,7 +62,7 @@ class TriggerFlowForEachProcess(TriggerFlowBaseProcess):
62
62
  await data.async_emit(
63
63
  send_item_trigger,
64
64
  data.value,
65
- data.layer_marks.copy(),
65
+ data._layer_marks.copy(),
66
66
  )
67
67
  data.layer_out()
68
68
 
@@ -103,7 +103,7 @@ class TriggerFlowForEachProcess(TriggerFlowBaseProcess):
103
103
  await data.async_emit(
104
104
  end_for_each_trigger,
105
105
  list(for_each_results[for_each_instance_id].values()),
106
- data.layer_marks.copy(),
106
+ data._layer_marks.copy(),
107
107
  )
108
108
  for_each_results.delete(for_each_instance_id)
109
109
 
@@ -58,7 +58,7 @@ class TriggerFlowMatchCaseProcess(TriggerFlowBaseProcess):
58
58
  await data.async_emit(
59
59
  f"Match-{ match_id }-Case-{ case_id }",
60
60
  data.value,
61
- layer_marks=data.layer_marks.copy(),
61
+ _layer_marks=data._layer_marks.copy(),
62
62
  )
63
63
  return
64
64
  elif mode == "hit_all":
@@ -71,7 +71,7 @@ class TriggerFlowMatchCaseProcess(TriggerFlowBaseProcess):
71
71
  data.async_emit(
72
72
  f"Match-{ match_id }-Case-{ case_id }",
73
73
  data.value,
74
- layer_marks=data.layer_marks.copy(),
74
+ _layer_marks=data._layer_marks.copy(),
75
75
  )
76
76
  )
77
77
  data.layer_out()
@@ -81,13 +81,13 @@ class TriggerFlowMatchCaseProcess(TriggerFlowBaseProcess):
81
81
  await data.async_emit(
82
82
  f"Match-{ match_id }-Else",
83
83
  data.value,
84
- layer_marks=data.layer_marks.copy(),
84
+ _layer_marks=data._layer_marks.copy(),
85
85
  )
86
86
  else:
87
87
  await data.async_emit(
88
88
  f"Match-{ match_id }-Result",
89
89
  data.value,
90
- layer_marks=data.layer_marks.copy(),
90
+ _layer_marks=data._layer_marks.copy(),
91
91
  )
92
92
 
93
93
  self.to(match_case)
@@ -164,7 +164,7 @@ class TriggerFlowMatchCaseProcess(TriggerFlowBaseProcess):
164
164
  await data.async_emit(
165
165
  f"Match-{ match_id }-Result",
166
166
  list(match_results.values()),
167
- layer_marks=data.layer_marks.copy(),
167
+ _layer_marks=data._layer_marks.copy(),
168
168
  )
169
169
  del data._system_runtime_data[f"match_results.{ data.upper_layer_mark }"]
170
170
  else:
@@ -172,7 +172,7 @@ class TriggerFlowMatchCaseProcess(TriggerFlowBaseProcess):
172
172
  await data.async_emit(
173
173
  f"Match-{ match_id }-Result",
174
174
  data.value,
175
- layer_marks=data.layer_marks.copy(),
175
+ _layer_marks=data._layer_marks.copy(),
176
176
  )
177
177
 
178
178
  for trigger in branch_ends: