agently 4.0.6.7__py3-none-any.whl → 4.0.7__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 (30) hide show
  1. agently/base.py +3 -5
  2. agently/builtins/agent_extensions/ConfigurePromptExtension.py +40 -9
  3. agently/builtins/hookers/SystemMessageHooker.py +51 -7
  4. agently/builtins/plugins/ModelRequester/OpenAICompatible.py +32 -0
  5. agently/builtins/plugins/PromptGenerator/AgentlyPromptGenerator.py +64 -1
  6. agently/builtins/plugins/ResponseParser/AgentlyResponseParser.py +22 -14
  7. agently/core/Agent.py +67 -27
  8. agently/core/ModelRequest.py +216 -26
  9. agently/core/PluginManager.py +2 -0
  10. agently/core/Prompt.py +15 -45
  11. agently/core/TriggerFlow/BluePrint.py +2 -0
  12. agently/core/TriggerFlow/Chunk.py +5 -4
  13. agently/core/TriggerFlow/Execution.py +29 -12
  14. agently/core/TriggerFlow/TriggerFlow.py +24 -10
  15. agently/core/TriggerFlow/process/BaseProcess.py +63 -21
  16. agently/core/TriggerFlow/process/ForEachProcess.py +30 -24
  17. agently/core/TriggerFlow/process/MatchCaseProcess.py +6 -6
  18. agently/integrations/chromadb.py +15 -0
  19. agently/types/data/response.py +10 -1
  20. agently/types/plugins/PromptGenerator.py +5 -1
  21. agently/types/plugins/ResponseParser.py +26 -6
  22. agently/types/trigger_flow/trigger_flow.py +6 -6
  23. agently/utils/DataFormatter.py +77 -0
  24. agently/utils/PythonSandbox.py +101 -0
  25. agently/utils/Settings.py +19 -2
  26. agently/utils/__init__.py +1 -0
  27. {agently-4.0.6.7.dist-info → agently-4.0.7.dist-info}/METADATA +1 -1
  28. {agently-4.0.6.7.dist-info → agently-4.0.7.dist-info}/RECORD +30 -29
  29. {agently-4.0.6.7.dist-info → agently-4.0.7.dist-info}/WHEEL +0 -0
  30. {agently-4.0.6.7.dist-info → agently-4.0.7.dist-info}/licenses/LICENSE +0 -0
agently/base.py CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  from typing import Any, Literal, Type, TYPE_CHECKING, TypeVar, Generic, cast
16
16
 
17
- from agently.utils import Settings, create_logger, FunctionShifter
17
+ from agently.utils import Settings, create_logger, FunctionShifter, DataFormatter
18
18
  from agently.core import PluginManager, EventCenter, Tool, Prompt, ModelRequest, BaseAgent
19
19
  from agently._default_init import _load_default_settings, _load_default_plugins, _hook_default_event_handlers
20
20
 
@@ -117,6 +117,8 @@ class AgentlyMain(Generic[A]):
117
117
  self.tool = tool
118
118
  self.AgentType = AgentType
119
119
 
120
+ self.set_settings = self.settings.set_settings
121
+
120
122
  def set_debug_console(self, debug_console_status: Literal["ON", "OFF"]):
121
123
  match debug_console_status:
122
124
  case "OFF":
@@ -130,10 +132,6 @@ class AgentlyMain(Generic[A]):
130
132
  self.logger.setLevel(log_level)
131
133
  return self
132
134
 
133
- def set_settings(self, key: str, value: "SerializableValue"):
134
- self.settings.set_settings(key, value)
135
- return self
136
-
137
135
  def create_prompt(self, name: str = "agently_prompt") -> Prompt:
138
136
  return Prompt(
139
137
  self.plugin_manager,
@@ -21,6 +21,7 @@ from typing import Any
21
21
  from json import JSONDecodeError
22
22
 
23
23
  from agently.core import BaseAgent
24
+ from agently.utils import DataLocator
24
25
 
25
26
 
26
27
  class ConfigurePromptExtension(BaseAgent):
@@ -168,44 +169,74 @@ class ConfigurePromptExtension(BaseAgent):
168
169
  variable_mappings,
169
170
  )
170
171
 
171
- def load_yaml_prompt(self, path_or_content: str, mappings: dict[str, Any] | None = None):
172
+ def load_yaml_prompt(
173
+ self,
174
+ path_or_content: str | Path,
175
+ mappings: dict[str, Any] | None = None,
176
+ *,
177
+ prompt_key_path: str | None = None,
178
+ encoding: str | None = "utf-8",
179
+ ):
172
180
  path = Path(path_or_content)
173
181
  if path.exists() and path.is_file():
174
182
  try:
175
- with path.open("r", encoding="utf-8") as file:
183
+ with path.open("r", encoding=encoding) as file:
176
184
  prompt = yaml.safe_load(file)
177
185
  except yaml.YAMLError as e:
178
186
  raise ValueError(f"Cannot load YAML file '{ path_or_content }'.\nError: { e }")
179
187
  else:
180
188
  try:
181
- prompt = yaml.safe_load(path_or_content)
189
+ prompt = yaml.safe_load(str(path_or_content))
182
190
  except yaml.YAMLError as e:
183
191
  raise ValueError(f"Cannot load YAML content or file path not existed.\nError: { e }")
192
+ if not isinstance(prompt, dict):
193
+ raise TypeError(
194
+ "Cannot execute YAML prompt configures, expect prompt configures as a dictionary data but got:"
195
+ f"{ prompt }"
196
+ )
197
+ if prompt_key_path is not None:
198
+ prompt = DataLocator.locate_path_in_dict(prompt, prompt_key_path)
184
199
  if isinstance(prompt, dict):
185
200
  self._execute_prompt_configure(prompt, mappings)
186
201
  else:
187
202
  raise TypeError(
188
- "Cannot execute YAML prompt configures, expect prompt configures as a dictionary data but got:"
203
+ f"Cannot execute YAML prompt configures, expect prompt configures{ ' from [' + prompt_key_path + '] ' if prompt_key_path is not None else '' } as a dictionary data but got:"
189
204
  f"{ prompt }"
190
205
  )
206
+ return self
191
207
 
192
- def load_json_prompt(self, path_or_content: str, mappings: dict[str, Any] | None = None):
208
+ def load_json_prompt(
209
+ self,
210
+ path_or_content: str | Path,
211
+ mappings: dict[str, Any] | None = None,
212
+ *,
213
+ prompt_key_path: str | None = None,
214
+ encoding: str | None = "utf-8",
215
+ ):
193
216
  path = Path(path_or_content)
194
217
  if path.exists() and path.is_file():
195
218
  try:
196
- with path.open("r", encoding="utf-8") as file:
219
+ with path.open("r", encoding=encoding) as file:
197
220
  prompt = json5.load(file)
198
221
  except JSONDecodeError as e:
199
222
  raise ValueError(f"Cannot load JSON file '{ path_or_content }'.\nError: { e }")
200
223
  else:
201
224
  try:
202
- prompt = json5.loads(path_or_content)
203
- except yaml.YAMLError as e:
225
+ prompt = json5.loads(str(path_or_content))
226
+ except JSONDecodeError as e:
204
227
  raise ValueError(f"Cannot load JSON content or file path not existed.\nError: { e }")
228
+ if not isinstance(prompt, dict):
229
+ raise TypeError(
230
+ "Cannot execute JSON prompt configures, expect prompt configures as a dictionary data but got:"
231
+ f"{ prompt }"
232
+ )
233
+ if prompt_key_path is not None:
234
+ prompt = DataLocator.locate_path_in_dict(prompt, prompt_key_path)
205
235
  if isinstance(prompt, dict):
206
236
  self._execute_prompt_configure(prompt, mappings)
207
237
  else:
208
238
  raise TypeError(
209
- "Cannot execute JSON prompt configures, expect prompt configures as a dictionary data but got:"
239
+ f"Cannot execute JSON prompt configures, expect prompt configures{ ' from [' + prompt_key_path + '] ' if prompt_key_path is not None else '' }as a dictionary data but got:"
210
240
  f"{ prompt }"
211
241
  )
242
+ return self
@@ -17,6 +17,32 @@ from typing import TYPE_CHECKING
17
17
 
18
18
  from agently.types.plugins import EventHooker
19
19
 
20
+ COLORS = {
21
+ "black": 30,
22
+ "red": 31,
23
+ "green": 32,
24
+ "yellow": 33,
25
+ "blue": 34,
26
+ "magenta": 35,
27
+ "cyan": 36,
28
+ "white": 37,
29
+ "gray": 90,
30
+ }
31
+
32
+
33
+ def color_text(text: str, color: str | None = None, bold: bool = False, underline: bool = False) -> str:
34
+ codes = []
35
+ if bold:
36
+ codes.append("1")
37
+ if underline:
38
+ codes.append("4")
39
+ if color and color in COLORS:
40
+ codes.append(str(COLORS[color]))
41
+ if not codes:
42
+ return text
43
+ return f"\x1b[{';'.join(codes)}m{text}\x1b[0m"
44
+
45
+
20
46
  if TYPE_CHECKING:
21
47
  from agently.types.data import EventMessage, AgentlySystemEvent
22
48
 
@@ -72,12 +98,18 @@ class SystemMessageHooker(EventHooker):
72
98
  and SystemMessageHooker._current_meta["row_id"] == message_data["response_id"]
73
99
  and SystemMessageHooker._current_meta["stage"] == content["stage"]
74
100
  ):
75
- print(content["detail"], end="")
101
+ print(color_text(content["detail"], color="gray"), end="", flush=True)
76
102
  else:
77
- print(
78
- f"[Agent-{ message_data['agent_name'] }] - [Request-{ message_data['response_id'] }]\nStage: { content['stage'] }\nDetail:\n{ content['detail'] }",
79
- end="",
103
+ header = color_text(
104
+ f"[Agent-{ message_data['agent_name'] }] - [Request-{ message_data['response_id'] }]",
105
+ color="blue",
106
+ bold=True,
80
107
  )
108
+ stage_label = color_text("Stage:", color="cyan", bold=True)
109
+ stage_val = color_text(content["stage"], color="yellow", underline=True)
110
+ detail_label = color_text("Detail:\n", color="cyan", bold=True)
111
+ detail = color_text(content["detail"], color="green")
112
+ print(f"{header}\n{stage_label} {stage_val}\n{detail_label}{detail}", end="")
81
113
  SystemMessageHooker._current_meta["table_name"] = message_data["agent_name"]
82
114
  SystemMessageHooker._current_meta["row_id"] = message_data["response_id"]
83
115
  SystemMessageHooker._current_meta["stage"] = content["stage"]
@@ -99,28 +131,40 @@ class SystemMessageHooker(EventHooker):
99
131
  },
100
132
  )
101
133
  if settings["runtime.show_model_logs"]:
134
+ header = color_text(
135
+ f"[Agent-{ message_data['agent_name'] }] - [Response-{ message_data['response_id'] }]",
136
+ color="blue",
137
+ bold=True,
138
+ )
139
+ stage_label = color_text("Stage:", color="cyan", bold=True)
140
+ stage_val = color_text(content["stage"], color="yellow", underline=True)
141
+ detail_label = color_text("Detail:\n", color="cyan", bold=True)
142
+ detail = color_text(f"{content['detail']}", color="gray")
102
143
  await event_center.async_emit(
103
144
  "log",
104
145
  {
105
146
  "level": "INFO",
106
- "content": f"[Agent-{ message_data['agent_name'] }] - [Response-{ message_data['response_id'] }]\nStage: { content['stage'] }\nDetail:\n{ content['detail'] }",
147
+ "content": f"{header}\n{stage_label} {stage_val}\n{detail_label}{detail}",
107
148
  },
108
149
  )
109
150
  case "TOOL":
110
151
  if settings["runtime.show_tool_logs"]:
152
+ tool_title = color_text("[Tool Using Result]:", color="blue", bold=True)
153
+ tool_body = color_text(str(message.content["data"]), color="gray")
111
154
  await event_center.async_emit(
112
155
  "log",
113
156
  {
114
157
  "level": "INFO",
115
- "content": f"[Tool Using Result]:\n{ message.content['data'] }",
158
+ "content": f"{tool_title}\n{tool_body}",
116
159
  },
117
160
  )
118
161
  case "TRIGGER_FLOW":
119
162
  if settings["runtime.show_trigger_flow_logs"]:
163
+ trigger = color_text(f"[TriggerFlow] { message.content['data'] }", color="yellow", bold=True)
120
164
  await event_center.async_emit(
121
165
  "log",
122
166
  {
123
167
  "level": "INFO",
124
- "content": f"[TriggerFlow] { message.content['data'] }",
168
+ "content": trigger,
125
169
  },
126
170
  )
@@ -45,6 +45,7 @@ if TYPE_CHECKING:
45
45
  class ContentMapping(TypedDict):
46
46
  id: str | None
47
47
  role: str | None
48
+ reasoning: str | None
48
49
  delta: str | None
49
50
  tool_calls: str | None
50
51
  done: str | None
@@ -114,6 +115,7 @@ class OpenAICompatible(ModelRequester):
114
115
  "content_mapping": {
115
116
  "id": "id",
116
117
  "role": "choices[0].delta.role",
118
+ "reasoning": "choices[0].delta.reasoning_content",
117
119
  "delta": "choices[0].delta.content",
118
120
  "tool_calls": "choices[0].delta.tool_calls",
119
121
  "done": None,
@@ -124,6 +126,7 @@ class OpenAICompatible(ModelRequester):
124
126
  },
125
127
  "extra_done": None,
126
128
  },
129
+ "yield_extra_content_separately": True,
127
130
  "content_mapping_style": "dot",
128
131
  "timeout": {
129
132
  "connect": 30.0,
@@ -146,6 +149,10 @@ class OpenAICompatible(ModelRequester):
146
149
  self.model_type = cast(str, self.plugin_settings.get("model_type"))
147
150
  self._messenger = event_center.create_messenger(self.name)
148
151
 
152
+ # check if has attachment prompt
153
+ if self.prompt["attachment"]:
154
+ self.plugin_settings["rich_content"] = True
155
+
149
156
  @staticmethod
150
157
  def _on_register():
151
158
  pass
@@ -501,6 +508,7 @@ class OpenAICompatible(ModelRequester):
501
508
  async def broadcast_response(self, response_generator: AsyncGenerator) -> "AgentlyResponseGenerator":
502
509
  meta = {}
503
510
  message_record = {}
511
+ reasoning_buffer = ""
504
512
  content_buffer = ""
505
513
 
506
514
  content_mapping = cast(
@@ -512,6 +520,7 @@ class OpenAICompatible(ModelRequester):
512
520
  )
513
521
  id_mapping = content_mapping["id"]
514
522
  role_mapping = content_mapping["role"]
523
+ reasoning_mapping = content_mapping["reasoning"]
515
524
  delta_mapping = content_mapping["delta"]
516
525
  tool_calls_mapping = content_mapping["tool_calls"]
517
526
  done_mapping = content_mapping["done"]
@@ -519,6 +528,7 @@ class OpenAICompatible(ModelRequester):
519
528
  finish_reason_mapping = content_mapping["finish_reason"]
520
529
  extra_delta_mapping = content_mapping["extra_delta"]
521
530
  extra_done_mapping = content_mapping["extra_done"]
531
+ yield_extra_content_separately = self.plugin_settings.get("yield_extra_content_separately", True)
522
532
 
523
533
  content_mapping_style = str(self.plugin_settings.get("content_mapping_style"))
524
534
  if content_mapping_style not in ("dot", "slash"):
@@ -548,6 +558,15 @@ class OpenAICompatible(ModelRequester):
548
558
  )
549
559
  if role:
550
560
  meta.update({"role": role})
561
+ if reasoning_mapping:
562
+ reasoning = DataLocator.locate_path_in_dict(
563
+ loaded_message,
564
+ reasoning_mapping,
565
+ style=content_mapping_style,
566
+ )
567
+ if reasoning:
568
+ reasoning_buffer += str(reasoning)
569
+ yield "reasoning_delta", reasoning
551
570
  if delta_mapping:
552
571
  delta = DataLocator.locate_path_in_dict(
553
572
  loaded_message,
@@ -574,6 +593,8 @@ class OpenAICompatible(ModelRequester):
574
593
  )
575
594
  if extra_value:
576
595
  yield "extra", {extra_key: extra_value}
596
+ if yield_extra_content_separately:
597
+ yield extra_key, extra_value # type: ignore
577
598
  else:
578
599
  done_content = None
579
600
  if self.model_type == "embeddings" and done_mapping is None:
@@ -589,6 +610,17 @@ class OpenAICompatible(ModelRequester):
589
610
  yield "done", done_content
590
611
  else:
591
612
  yield "done", content_buffer
613
+ reasoning_content = None
614
+ if reasoning_mapping:
615
+ reasoning_content = DataLocator.locate_path_in_dict(
616
+ message_record,
617
+ reasoning_mapping,
618
+ style=content_mapping_style,
619
+ )
620
+ if reasoning_content:
621
+ yield "reasoning_done", reasoning_content
622
+ else:
623
+ yield "reasoning_done", reasoning_buffer
592
624
  match self.model_type:
593
625
  case "embeddings":
594
626
  yield "original_done", message_record
@@ -12,6 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ import json
15
16
  import yaml
16
17
 
17
18
  from typing import (
@@ -38,6 +39,7 @@ from agently.utils import SettingsNamespace, DataFormatter
38
39
 
39
40
  if TYPE_CHECKING:
40
41
  from pydantic import BaseModel
42
+ from agently.types.data import SerializableData
41
43
  from agently.core import Prompt
42
44
  from agently.utils import Settings
43
45
 
@@ -649,7 +651,12 @@ class AgentlyPromptGenerator(PromptGenerator):
649
651
  }
650
652
  )
651
653
 
652
- return create_model(name, **fields, **validators)
654
+ return create_model(
655
+ name,
656
+ __config__={'extra': 'allow'},
657
+ **fields,
658
+ **validators,
659
+ )
653
660
  else:
654
661
  item_type = Any
655
662
  if len(schema) > 0:
@@ -698,3 +705,59 @@ class AgentlyPromptGenerator(PromptGenerator):
698
705
  "AgentlyOutput",
699
706
  {"list": DataFormatter.sanitize(output_prompt, remain_type=True)},
700
707
  )
708
+
709
+ def _to_serializable_output_prompt(self, output_prompt_part: Any):
710
+ if not isinstance(output_prompt_part, (Mapping, Sequence)) or isinstance(output_prompt_part, str):
711
+ return output_prompt_part
712
+
713
+ if isinstance(output_prompt_part, Mapping):
714
+ result = {}
715
+ for key, value in output_prompt_part.items():
716
+ result[key] = self._to_serializable_output_prompt(value)
717
+ return result
718
+ else:
719
+ if isinstance(output_prompt_part, tuple):
720
+ match len(output_prompt_part):
721
+ case 0:
722
+ return []
723
+ case 1:
724
+ return {
725
+ "$type": output_prompt_part[0],
726
+ }
727
+ case _:
728
+ desc_text = ";".join([item for item in output_prompt_part[1:] if item])
729
+ if desc_text:
730
+ return {
731
+ "$type": output_prompt_part[0],
732
+ "$desc": desc_text,
733
+ }
734
+ else:
735
+ return {
736
+ "$type": output_prompt_part[0],
737
+ }
738
+ else:
739
+ return list(output_prompt_part)
740
+
741
+ def to_serializable_prompt_data(self, inherit: bool = False) -> "SerializableData":
742
+ prompt_data = self.prompt.get(
743
+ default={},
744
+ inherit=inherit,
745
+ )
746
+ if "output" in prompt_data:
747
+ prompt_data["output"] = self._to_serializable_output_prompt(prompt_data["output"])
748
+ return DataFormatter.sanitize(prompt_data)
749
+
750
+ def to_json_prompt(self, inherit: bool = False):
751
+ return json.dumps(
752
+ self.to_serializable_prompt_data(inherit),
753
+ indent=2,
754
+ ensure_ascii=False,
755
+ )
756
+
757
+ def to_yaml_prompt(self, inherit: bool = False):
758
+ return yaml.safe_dump(
759
+ self.to_serializable_prompt_data(inherit),
760
+ indent=2,
761
+ allow_unicode=True,
762
+ sort_keys=False,
763
+ )
@@ -282,8 +282,10 @@ class AgentlyResponseParser(ResponseParser):
282
282
 
283
283
  async def get_async_generator(
284
284
  self,
285
- type: Literal['all', 'delta', 'typed_delta', 'original', 'instant', 'streaming_parse'] | None = "delta",
286
- content: Literal['all', 'delta', 'typed_delta', 'original', 'instant', 'streaming_parse'] | None = "delta",
285
+ type: Literal['all', 'delta', 'specific', 'original', 'instant', 'streaming_parse'] | None = "delta",
286
+ content: Literal['all', 'delta', 'specific', 'original', 'instant', 'streaming_parse'] | None = "delta",
287
+ *,
288
+ specific: list[str] | str | None = ["reasoning_delta", "delta", "reasoning_done", "done", "tool_calls"],
287
289
  ) -> AsyncGenerator:
288
290
  await self._ensure_consumer()
289
291
  parsed_generator = cast(GeneratorConsumer, self._response_consumer).get_async_generator()
@@ -300,11 +302,13 @@ class AgentlyResponseParser(ResponseParser):
300
302
  case "delta":
301
303
  if event == "delta":
302
304
  yield data
303
- case "typed_delta":
304
- if event == "delta":
305
- yield "delta", data
306
- elif event == "tool_calls":
307
- yield "tool_calls", data
305
+ case "specific":
306
+ if specific is None:
307
+ specific = ["delta"]
308
+ elif isinstance(specific, str):
309
+ specific = [specific]
310
+ if event in specific:
311
+ yield event, data
308
312
  case "instant" | "streaming_parse":
309
313
  if self._streaming_json_parser is not None:
310
314
  streaming_parsed = None
@@ -325,8 +329,10 @@ class AgentlyResponseParser(ResponseParser):
325
329
 
326
330
  def get_generator(
327
331
  self,
328
- type: Literal['all', 'delta', 'typed_delta', 'original', 'instant', 'streaming_parse'] | None = "delta",
329
- content: Literal['all', 'delta', 'typed_delta', 'original', 'instant', 'streaming_parse'] | None = "delta",
332
+ type: Literal['all', 'delta', 'specific', 'original', 'instant', 'streaming_parse'] | None = "delta",
333
+ content: Literal['all', 'delta', 'specific', 'original', 'instant', 'streaming_parse'] | None = "delta",
334
+ *,
335
+ specific: list[str] | str | None = ["reasoning_delta", "delta", "reasoning_done", "done", "tool_calls"],
330
336
  ) -> Generator:
331
337
  asyncio.run(self._ensure_consumer())
332
338
  parsed_generator = cast(GeneratorConsumer, self._response_consumer).get_generator()
@@ -343,11 +349,13 @@ class AgentlyResponseParser(ResponseParser):
343
349
  case "delta":
344
350
  if event == "delta":
345
351
  yield data
346
- case "typed_delta":
347
- if event == "delta":
348
- yield "delta", data
349
- elif event == "tool_calls":
350
- yield "tool_calls", data
352
+ case "specific":
353
+ if specific is None:
354
+ specific = ["delta"]
355
+ elif isinstance(specific, str):
356
+ specific = [specific]
357
+ if event in specific:
358
+ yield event, data
351
359
  case "instant" | "streaming_parse":
352
360
  if self._streaming_json_parser is not None:
353
361
  streaming_parsed = None