zrb 1.8.15__py3-none-any.whl → 1.9.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.
zrb/task/llm/context.py CHANGED
@@ -1,14 +1,9 @@
1
1
  import datetime
2
- import inspect
3
2
  import os
4
3
  import platform
5
4
  import re
6
- from collections.abc import Callable
7
5
  from typing import Any
8
6
 
9
- from zrb.context.any_context import AnyContext
10
- from zrb.context.any_shared_context import AnySharedContext
11
- from zrb.util.attr import get_attr
12
7
  from zrb.util.file import read_dir, read_file_with_line_numbers
13
8
 
14
9
 
@@ -61,42 +56,3 @@ def extract_default_context(user_message: str) -> tuple[str, dict[str, Any]]:
61
56
  }
62
57
 
63
58
  return modified_user_message, context
64
-
65
-
66
- def get_conversation_context(
67
- ctx: AnyContext,
68
- conversation_context_attr: (
69
- dict[str, Any] | Callable[[AnySharedContext], dict[str, Any]] | None
70
- ),
71
- ) -> dict[str, Any]:
72
- """
73
- Retrieves the conversation context.
74
- If a value in the context dict is callable, it executes it with ctx.
75
- """
76
- raw_context = get_attr(ctx, conversation_context_attr, {}, auto_render=False)
77
- if not isinstance(raw_context, dict):
78
- ctx.log_warning(
79
- f"Conversation context resolved to type {type(raw_context)}, "
80
- "expected dict. Returning empty context."
81
- )
82
- return {}
83
- # If conversation_context contains callable value, execute them.
84
- processed_context: dict[str, Any] = {}
85
- for key, value in raw_context.items():
86
- if callable(value):
87
- try:
88
- # Check if the callable expects 'ctx'
89
- sig = inspect.signature(value)
90
- if "ctx" in sig.parameters:
91
- processed_context[key] = value(ctx)
92
- else:
93
- processed_context[key] = value()
94
- except Exception as e:
95
- ctx.log_warning(
96
- f"Error executing callable for context key '{key}': {e}. "
97
- "Skipping."
98
- )
99
- processed_context[key] = None
100
- else:
101
- processed_context[key] = value
102
- return processed_context
@@ -1,8 +1,6 @@
1
1
  import json
2
2
  import traceback
3
- from typing import TYPE_CHECKING, Any
4
-
5
- from pydantic import BaseModel
3
+ from typing import TYPE_CHECKING
6
4
 
7
5
  from zrb.attr.type import BoolAttr, IntAttr
8
6
  from zrb.context.any_context import AnyContext
@@ -20,95 +18,63 @@ from zrb.util.cli.style import stylize_faint
20
18
  if TYPE_CHECKING:
21
19
  from pydantic_ai.models import Model
22
20
  from pydantic_ai.settings import ModelSettings
23
- else:
24
- Model = Any
25
- ModelSettings = Any
26
-
27
-
28
- class EnrichmentConfig(BaseModel):
29
- model_config = {"arbitrary_types_allowed": True}
30
- model: Model | str | None = None
31
- settings: ModelSettings | None = None
32
- prompt: str
33
- retries: int = 3
34
-
35
-
36
- class EnrichmentResult(BaseModel):
37
- response: dict[str, Any] # or further decompose as needed
38
21
 
39
22
 
40
23
  async def enrich_context(
41
24
  ctx: AnyContext,
42
- config: EnrichmentConfig,
43
- conversation_context: dict[str, Any],
25
+ model: "Model | str | None",
26
+ settings: "ModelSettings | None",
27
+ prompt: str,
28
+ previous_long_term_context: str,
44
29
  history_list: ListOfDict,
45
30
  rate_limitter: LLMRateLimiter | None = None,
46
- ) -> dict[str, Any]:
47
- """Runs an LLM call to extract key info and merge it into the context."""
31
+ retries: int = 3,
32
+ ) -> str:
33
+ """Runs an LLM call to update the long-term context and returns the new context string."""
48
34
  from pydantic_ai import Agent
49
35
 
50
36
  ctx.log_info("Attempting to enrich conversation context...")
51
- # Prepare context and history for the enrichment prompt
52
- history_summary = conversation_context.get("history_summary")
53
- try:
54
- context_json = json.dumps(conversation_context)
55
- history_json = json.dumps(history_list)
56
- # The user prompt will now contain the dynamic data
57
- user_prompt_data = "\n".join(
58
- [
59
- "Extract context from the following conversation info.",
60
- "Extract only contexts that will be relevant across multiple conversations, like", # noqa
61
- "- user name",
62
- "- user hobby",
63
- "- user's long life goal",
64
- "- standard/SOP",
65
- "- etc.",
66
- "Always maintain the relevant context and remove the irrelevant ones.",
67
- "Restructure the context in a helpful way",
68
- "Keep the context small",
69
- f"Existing Context: {context_json}",
70
- f"Conversation History: {history_json}",
71
- ]
72
- )
73
- except Exception as e:
74
- ctx.log_warning(f"Error formatting context/history for enrichment: {e}")
75
- return conversation_context # Return original context if formatting fails
76
-
37
+ # Construct the user prompt according to the new prompt format
38
+ user_prompt = json.dumps(
39
+ {
40
+ "previous_long_term_context": previous_long_term_context,
41
+ "recent_conversation_history": history_list,
42
+ }
43
+ )
77
44
  enrichment_agent = Agent(
78
- model=config.model,
79
- system_prompt=config.prompt, # Use the main prompt as system prompt
80
- model_settings=config.settings,
81
- retries=config.retries,
82
- output_type=EnrichmentResult,
45
+ model=model,
46
+ system_prompt=prompt,
47
+ model_settings=settings,
48
+ retries=retries,
83
49
  )
84
50
 
85
51
  try:
86
- ctx.print(stylize_faint("[Context Enrichment Triggered]"), plain=True)
52
+ ctx.print(stylize_faint("💡 Enrich Context"), plain=True)
87
53
  enrichment_run = await run_agent_iteration(
88
54
  ctx=ctx,
89
55
  agent=enrichment_agent,
90
- user_prompt=user_prompt_data, # Pass the formatted data as user prompt
91
- history_list=[], # Enrichment agent doesn't need prior history itself
56
+ user_prompt=user_prompt,
57
+ history_list=[], # Enrichment agent works off the prompt, not history
92
58
  rate_limitter=rate_limitter,
93
59
  )
94
60
  if enrichment_run and enrichment_run.result.output:
95
- response = enrichment_run.result.output.response
61
+ new_long_term_context = str(enrichment_run.result.output)
96
62
  usage = enrichment_run.result.usage()
97
- ctx.print(stylize_faint(f"[Token Usage] {usage}"), plain=True)
98
- if response:
99
- conversation_context = response
100
- # Re inject history summary
101
- conversation_context["history_summary"] = history_summary
102
- ctx.log_info("Context enriched based on history.")
103
- ctx.log_info(
104
- f"Updated conversation context: {json.dumps(conversation_context)}"
105
- )
63
+ ctx.print(
64
+ stylize_faint(f"💡 Context Enrichment Token: {usage}"), plain=True
65
+ )
66
+ ctx.print(plain=True)
67
+ ctx.log_info("Context enriched based on history.")
68
+ ctx.log_info(f"Updated long-term context:\n{new_long_term_context}")
69
+ return new_long_term_context
106
70
  else:
107
- ctx.log_warning("Context enrichment returned no data")
71
+ ctx.log_warning("Context enrichment returned no data.")
108
72
  except Exception as e:
109
73
  ctx.log_warning(f"Error during context enrichment LLM call: {e}")
110
74
  traceback.print_exc()
111
- return conversation_context
75
+
76
+ # Return the original context if enrichment fails
77
+ return previous_long_term_context
112
78
 
113
79
 
114
80
  def get_context_enrichment_threshold(
@@ -121,7 +87,6 @@ def get_context_enrichment_threshold(
121
87
  return get_int_attr(
122
88
  ctx,
123
89
  context_enrichment_threshold_attr,
124
- # Use llm_config default if attribute is None
125
90
  llm_config.default_context_enrichment_threshold,
126
91
  auto_render=render_context_enrichment_threshold,
127
92
  )
@@ -136,7 +101,7 @@ def get_context_enrichment_threshold(
136
101
  def should_enrich_context(
137
102
  ctx: AnyContext,
138
103
  history_list: ListOfDict,
139
- should_enrich_context_attr: BoolAttr | None, # Allow None
104
+ should_enrich_context_attr: BoolAttr | None,
140
105
  render_enrich_context: bool,
141
106
  context_enrichment_threshold_attr: IntAttr | None,
142
107
  render_context_enrichment_threshold: bool,
@@ -165,16 +130,16 @@ def should_enrich_context(
165
130
  async def maybe_enrich_context(
166
131
  ctx: AnyContext,
167
132
  history_list: ListOfDict,
168
- conversation_context: dict[str, Any],
133
+ long_term_context: str,
169
134
  should_enrich_context_attr: BoolAttr | None,
170
135
  render_enrich_context: bool,
171
136
  context_enrichment_threshold_attr: IntAttr | None,
172
137
  render_context_enrichment_threshold: bool,
173
- model: str | Model | None,
174
- model_settings: ModelSettings | None,
138
+ model: "str | Model | None",
139
+ model_settings: "ModelSettings | None",
175
140
  context_enrichment_prompt: str,
176
141
  rate_limitter: LLMRateLimiter | None = None,
177
- ) -> dict[str, Any]:
142
+ ) -> str:
178
143
  """Enriches context based on history if enabled and threshold met."""
179
144
  shorten_history_list = replace_system_prompt_in_history_list(history_list)
180
145
  if should_enrich_context(
@@ -187,13 +152,11 @@ async def maybe_enrich_context(
187
152
  ):
188
153
  return await enrich_context(
189
154
  ctx=ctx,
190
- config=EnrichmentConfig(
191
- model=model,
192
- settings=model_settings,
193
- prompt=context_enrichment_prompt,
194
- ),
195
- conversation_context=conversation_context,
155
+ model=model,
156
+ settings=model_settings,
157
+ prompt=context_enrichment_prompt,
158
+ previous_long_term_context=long_term_context,
196
159
  history_list=shorten_history_list,
197
160
  rate_limitter=rate_limitter,
198
161
  )
199
- return conversation_context
162
+ return long_term_context
zrb/task/llm/error.py CHANGED
@@ -1,12 +1,10 @@
1
1
  import json
2
- from typing import TYPE_CHECKING, Any, Optional
2
+ from typing import TYPE_CHECKING, Optional
3
3
 
4
4
  from pydantic import BaseModel
5
5
 
6
6
  if TYPE_CHECKING:
7
7
  from openai import APIError
8
- else:
9
- APIError = Any
10
8
 
11
9
 
12
10
  # Define a structured error model for tool execution failures
@@ -17,7 +15,7 @@ class ToolExecutionError(BaseModel):
17
15
  details: Optional[str] = None
18
16
 
19
17
 
20
- def extract_api_error_details(error: APIError) -> str:
18
+ def extract_api_error_details(error: "APIError") -> str:
21
19
  """Extract detailed error information from an APIError."""
22
20
  details = f"{error.message}"
23
21
  # Try to parse the error body as JSON
zrb/task/llm/history.py CHANGED
@@ -4,7 +4,7 @@ from collections.abc import Callable
4
4
  from copy import deepcopy
5
5
  from typing import Any, Optional
6
6
 
7
- from pydantic import BaseModel
7
+ from pydantic import BaseModel, Field
8
8
 
9
9
  from zrb.attr.type import StrAttr
10
10
  from zrb.context.any_context import AnyContext
@@ -17,8 +17,18 @@ from zrb.util.run import run_async
17
17
 
18
18
  # Define the new ConversationHistoryData model
19
19
  class ConversationHistoryData(BaseModel):
20
- context: dict[str, Any] = {}
21
- history: ListOfDict = []
20
+ long_term_context: str = Field(
21
+ default="",
22
+ description="A markdown-formatted string containing curated, long-term context.",
23
+ )
24
+ conversation_summary: str = Field(
25
+ default="",
26
+ description="A free-text summary of the conversation history.",
27
+ )
28
+ history: ListOfDict = Field(
29
+ default_factory=list,
30
+ description="The recent, un-summarized conversation history.",
31
+ )
22
32
 
23
33
  @classmethod
24
34
  async def read_from_sources(
@@ -69,19 +79,17 @@ class ConversationHistoryData(BaseModel):
69
79
  try:
70
80
  if isinstance(data, cls):
71
81
  return data # Already a valid instance
72
- if isinstance(data, dict) and "history" in data:
73
- # Standard format {'context': ..., 'history': ...}
74
- # Ensure context exists, even if empty
75
- data.setdefault("context", {})
82
+ if isinstance(data, dict):
83
+ # This handles both the new format and the old {'context': ..., 'history': ...}
76
84
  return cls.model_validate(data)
77
85
  elif isinstance(data, list):
78
- # Handle old format (just a list) - wrap it
86
+ # Handle very old format (just a list) - wrap it
79
87
  ctx.log_warning(
80
- f"History from {source} contains old list format. "
81
- "Wrapping it into the new structure {'context': {}, 'history': [...]}. "
88
+ f"History from {source} contains legacy list format. "
89
+ "Wrapping it into the new structure. "
82
90
  "Consider updating the source format."
83
91
  )
84
- return cls(history=data, context={})
92
+ return cls(history=data)
85
93
  else:
86
94
  ctx.log_warning(
87
95
  f"History data from {source} has unexpected format "
@@ -1,7 +1,6 @@
1
1
  import json
2
- from typing import TYPE_CHECKING, Any
3
-
4
- from pydantic import BaseModel
2
+ import traceback
3
+ from typing import TYPE_CHECKING
5
4
 
6
5
  from zrb.attr.type import BoolAttr, IntAttr
7
6
  from zrb.context.any_context import AnyContext
@@ -19,9 +18,6 @@ from zrb.util.cli.style import stylize_faint
19
18
  if TYPE_CHECKING:
20
19
  from pydantic_ai.models import Model
21
20
  from pydantic_ai.settings import ModelSettings
22
- else:
23
- Model = Any
24
- ModelSettings = Any
25
21
 
26
22
 
27
23
  def get_history_summarization_threshold(
@@ -34,7 +30,6 @@ def get_history_summarization_threshold(
34
30
  return get_int_attr(
35
31
  ctx,
36
32
  history_summarization_threshold_attr,
37
- # Use llm_config default if attribute is None
38
33
  llm_config.default_history_summarization_threshold,
39
34
  auto_render=render_history_summarization_threshold,
40
35
  )
@@ -49,9 +44,9 @@ def get_history_summarization_threshold(
49
44
  def should_summarize_history(
50
45
  ctx: AnyContext,
51
46
  history_list: ListOfDict,
52
- should_summarize_history_attr: BoolAttr | None, # Allow None
47
+ should_summarize_history_attr: BoolAttr | None,
53
48
  render_summarize_history: bool,
54
- history_summarization_threshold_attr: IntAttr | None, # Allow None
49
+ history_summarization_threshold_attr: IntAttr | None,
55
50
  render_history_summarization_threshold: bool,
56
51
  ) -> bool:
57
52
  """Determines if history summarization should occur based on length and config."""
@@ -68,91 +63,76 @@ def should_summarize_history(
68
63
  return get_bool_attr(
69
64
  ctx,
70
65
  should_summarize_history_attr,
71
- # Use llm_config default if attribute is None
72
66
  llm_config.default_summarize_history,
73
67
  auto_render=render_summarize_history,
74
68
  )
75
69
 
76
70
 
77
- class SummarizationConfig(BaseModel):
78
- model_config = {"arbitrary_types_allowed": True}
79
- model: Model | str | None = None
80
- settings: ModelSettings | None = None
81
- prompt: str
82
- retries: int = 3
83
-
84
-
85
71
  async def summarize_history(
86
72
  ctx: AnyContext,
87
- config: SummarizationConfig,
88
- conversation_context: dict[str, Any],
73
+ model: "Model | str | None",
74
+ settings: "ModelSettings | None",
75
+ prompt: str,
76
+ previous_summary: str,
89
77
  history_list: ListOfDict,
90
78
  rate_limitter: LLMRateLimiter | None = None,
91
- ) -> dict[str, Any]:
92
- """Runs an LLM call to summarize history and update the context."""
79
+ retries: int = 3,
80
+ ) -> str:
81
+ """Runs an LLM call to update the conversation summary."""
93
82
  from pydantic_ai import Agent
94
83
 
95
84
  ctx.log_info("Attempting to summarize conversation history...")
96
-
85
+ # Construct the user prompt for the summarization agent
86
+ user_prompt = json.dumps(
87
+ {"previous_summary": previous_summary, "recent_history": history_list}
88
+ )
97
89
  summarization_agent = Agent(
98
- model=config.model,
99
- system_prompt=config.prompt,
100
- model_settings=config.settings,
101
- retries=config.retries,
90
+ model=model,
91
+ system_prompt=prompt,
92
+ model_settings=settings,
93
+ retries=retries,
102
94
  )
103
95
 
104
- # Prepare context and history for summarization prompt
105
96
  try:
106
- context_json = json.dumps(conversation_context)
107
- history_to_summarize_json = json.dumps(history_list)
108
- summarization_user_prompt = "\n".join(
109
- [
110
- f"Current Context: {context_json}",
111
- f"Conversation History to Summarize: {history_to_summarize_json}",
112
- ]
113
- )
114
- except Exception as e:
115
- ctx.log_warning(f"Error formatting context/history for summarization: {e}")
116
- return conversation_context # Return original context if formatting fails
117
-
118
- try:
119
- ctx.print(stylize_faint("[Summarization Triggered]"), plain=True)
97
+ ctx.print(stylize_faint("📝 Summarize"), plain=True)
120
98
  summary_run = await run_agent_iteration(
121
99
  ctx=ctx,
122
100
  agent=summarization_agent,
123
- user_prompt=summarization_user_prompt,
124
- history_list=[], # Summarization agent doesn't need prior history
101
+ user_prompt=user_prompt,
102
+ history_list=[],
125
103
  rate_limitter=rate_limitter,
126
104
  )
127
105
  if summary_run and summary_run.result.output:
128
- summary_text = str(summary_run.result.output)
106
+ new_summary = str(summary_run.result.output)
129
107
  usage = summary_run.result.usage()
130
- ctx.print(stylize_faint(f"[Token Usage] {usage}"), plain=True)
131
- # Update context with the new summary
132
- conversation_context["history_summary"] = summary_text
133
- ctx.log_info("History summarized and added/updated in context.")
134
- ctx.log_info(f"Conversation summary: {summary_text}")
108
+ ctx.print(stylize_faint(f"📝 Summarization Token: {usage}"), plain=True)
109
+ ctx.print(plain=True)
110
+ ctx.log_info("History summarized and updated.")
111
+ ctx.log_info(f"New conversation summary:\n{new_summary}")
112
+ return new_summary
135
113
  else:
136
114
  ctx.log_warning("History summarization failed or returned no data.")
137
115
  except Exception as e:
138
116
  ctx.log_warning(f"Error during history summarization: {e}")
117
+ traceback.print_exc()
139
118
 
140
- return conversation_context
119
+ # Return the original summary if summarization fails
120
+ return previous_summary
141
121
 
142
122
 
143
123
  async def maybe_summarize_history(
144
124
  ctx: AnyContext,
145
125
  history_list: ListOfDict,
146
- conversation_context: dict[str, Any],
147
- should_summarize_history_attr: BoolAttr | None, # Allow None
126
+ conversation_summary: str,
127
+ should_summarize_history_attr: BoolAttr | None,
148
128
  render_summarize_history: bool,
149
- history_summarization_threshold_attr: IntAttr | None, # Allow None
129
+ history_summarization_threshold_attr: IntAttr | None,
150
130
  render_history_summarization_threshold: bool,
151
- model: str | Model | None,
152
- model_settings: ModelSettings | None,
131
+ model: "str | Model | None",
132
+ model_settings: "ModelSettings | None",
153
133
  summarization_prompt: str,
154
134
  rate_limitter: LLMRateLimiter | None = None,
155
- ) -> tuple[ListOfDict, dict[str, Any]]:
135
+ ) -> tuple[ListOfDict, str]:
156
136
  """Summarizes history and updates context if enabled and threshold met."""
157
137
  shorten_history_list = replace_system_prompt_in_history_list(history_list)
158
138
  if should_summarize_history(
@@ -163,18 +143,15 @@ async def maybe_summarize_history(
163
143
  history_summarization_threshold_attr,
164
144
  render_history_summarization_threshold,
165
145
  ):
166
- # Use summarize_history defined above
167
- updated_context = await summarize_history(
146
+ new_summary = await summarize_history(
168
147
  ctx=ctx,
169
- config=SummarizationConfig(
170
- model=model,
171
- settings=model_settings,
172
- prompt=summarization_prompt,
173
- ),
174
- conversation_context=conversation_context,
175
- history_list=shorten_history_list, # Pass the full list for context
148
+ model=model,
149
+ settings=model_settings,
150
+ prompt=summarization_prompt,
151
+ previous_summary=conversation_summary,
152
+ history_list=shorten_history_list,
176
153
  rate_limitter=rate_limitter,
177
154
  )
178
- # Truncate the history list after summarization
179
- return [], updated_context
180
- return history_list, conversation_context
155
+ # After summarization, the history is cleared and replaced by the new summary
156
+ return [], new_summary
157
+ return history_list, conversation_summary
@@ -19,11 +19,11 @@ async def print_node(print_func: Callable, agent_run: Any, node: Any):
19
19
 
20
20
  if Agent.is_user_prompt_node(node):
21
21
  # A user prompt node => The user has provided input
22
- print_func(stylize_faint(f">> UserPromptNode: {node.user_prompt}"))
22
+ print_func(stylize_faint(f" >> UserPromptNode: {node.user_prompt}"))
23
23
  elif Agent.is_model_request_node(node):
24
24
  # A model request node => We can stream tokens from the model's request
25
25
  print_func(
26
- stylize_faint(">> ModelRequestNode: streaming partial request tokens")
26
+ stylize_faint(" >> ModelRequestNode: streaming partial request tokens")
27
27
  )
28
28
  async with node.stream(agent_run.ctx) as request_stream:
29
29
  is_streaming = False
@@ -33,7 +33,7 @@ async def print_node(print_func: Callable, agent_run: Any, node: Any):
33
33
  print_func("")
34
34
  print_func(
35
35
  stylize_faint(
36
- f"[Request] Starting part {event.index}: {event.part!r}"
36
+ f" [Request] Starting part {event.index}: {event.part!r}"
37
37
  ),
38
38
  )
39
39
  is_streaming = False
@@ -53,7 +53,7 @@ async def print_node(print_func: Callable, agent_run: Any, node: Any):
53
53
  if is_streaming:
54
54
  print_func("")
55
55
  print_func(
56
- stylize_faint(f"[Result] tool_name={event.tool_name}"),
56
+ stylize_faint(f" [Result] tool_name={event.tool_name}"),
57
57
  )
58
58
  is_streaming = False
59
59
  if is_streaming:
@@ -61,7 +61,9 @@ async def print_node(print_func: Callable, agent_run: Any, node: Any):
61
61
  elif Agent.is_call_tools_node(node):
62
62
  # A handle-response node => The model returned some data, potentially calls a tool
63
63
  print_func(
64
- stylize_faint(">> CallToolsNode: streaming partial response & tool usage")
64
+ stylize_faint(
65
+ " >> CallToolsNode: streaming partial response & tool usage"
66
+ )
65
67
  )
66
68
  async with node.stream(agent_run.ctx) as handle_stream:
67
69
  async for event in handle_stream:
@@ -82,16 +84,16 @@ async def print_node(print_func: Callable, agent_run: Any, node: Any):
82
84
  del event.part.args["_dummy"]
83
85
  print_func(
84
86
  stylize_faint(
85
- f"[Tools] The LLM calls tool={event.part.tool_name!r} with args={event.part.args} (tool_call_id={event.part.tool_call_id!r})" # noqa
87
+ f" [Tools] The LLM calls tool={event.part.tool_name!r} with args={event.part.args} (tool_call_id={event.part.tool_call_id!r})" # noqa
86
88
  )
87
89
  )
88
90
  elif isinstance(event, FunctionToolResultEvent):
89
91
  print_func(
90
92
  stylize_faint(
91
- f"[Tools] Tool call {event.tool_call_id!r} returned => {event.result.content}" # noqa
93
+ f" [Tools] Tool call {event.tool_call_id!r} returned => {event.result.content}" # noqa
92
94
  )
93
95
  )
94
96
  elif Agent.is_end_node(node):
95
97
  # Once an End node is reached, the agent run is complete
96
- print_func(stylize_faint("[End of Response]"))
98
+ print_func(stylize_faint(" [End of Response]"))
97
99
  # print_func(stylize_faint(f"{agent_run.result.data}"))