zrb 1.15.3__py3-none-any.whl → 1.21.29__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.

Potentially problematic release.


This version of zrb might be problematic. Click here for more details.

Files changed (108) hide show
  1. zrb/__init__.py +2 -6
  2. zrb/attr/type.py +10 -7
  3. zrb/builtin/__init__.py +2 -0
  4. zrb/builtin/git.py +12 -1
  5. zrb/builtin/group.py +31 -15
  6. zrb/builtin/llm/attachment.py +40 -0
  7. zrb/builtin/llm/chat_completion.py +274 -0
  8. zrb/builtin/llm/chat_session.py +126 -167
  9. zrb/builtin/llm/chat_session_cmd.py +288 -0
  10. zrb/builtin/llm/chat_trigger.py +79 -0
  11. zrb/builtin/llm/history.py +4 -4
  12. zrb/builtin/llm/llm_ask.py +217 -135
  13. zrb/builtin/llm/tool/api.py +74 -70
  14. zrb/builtin/llm/tool/cli.py +35 -21
  15. zrb/builtin/llm/tool/code.py +55 -73
  16. zrb/builtin/llm/tool/file.py +278 -344
  17. zrb/builtin/llm/tool/note.py +84 -0
  18. zrb/builtin/llm/tool/rag.py +27 -34
  19. zrb/builtin/llm/tool/sub_agent.py +54 -41
  20. zrb/builtin/llm/tool/web.py +74 -98
  21. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +7 -7
  22. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +5 -5
  23. zrb/builtin/project/add/fastapp/fastapp_util.py +1 -1
  24. zrb/builtin/searxng/config/settings.yml +5671 -0
  25. zrb/builtin/searxng/start.py +21 -0
  26. zrb/builtin/shell/autocomplete/bash.py +4 -3
  27. zrb/builtin/shell/autocomplete/zsh.py +4 -3
  28. zrb/config/config.py +202 -27
  29. zrb/config/default_prompt/file_extractor_system_prompt.md +109 -9
  30. zrb/config/default_prompt/interactive_system_prompt.md +24 -30
  31. zrb/config/default_prompt/persona.md +1 -1
  32. zrb/config/default_prompt/repo_extractor_system_prompt.md +31 -31
  33. zrb/config/default_prompt/repo_summarizer_system_prompt.md +27 -8
  34. zrb/config/default_prompt/summarization_prompt.md +57 -16
  35. zrb/config/default_prompt/system_prompt.md +36 -30
  36. zrb/config/llm_config.py +119 -23
  37. zrb/config/llm_context/config.py +127 -90
  38. zrb/config/llm_context/config_parser.py +1 -7
  39. zrb/config/llm_context/workflow.py +81 -0
  40. zrb/config/llm_rate_limitter.py +100 -47
  41. zrb/context/any_shared_context.py +7 -1
  42. zrb/context/context.py +8 -2
  43. zrb/context/shared_context.py +3 -7
  44. zrb/group/any_group.py +3 -3
  45. zrb/group/group.py +3 -3
  46. zrb/input/any_input.py +5 -1
  47. zrb/input/base_input.py +18 -6
  48. zrb/input/option_input.py +13 -1
  49. zrb/input/text_input.py +7 -24
  50. zrb/runner/cli.py +21 -20
  51. zrb/runner/common_util.py +24 -19
  52. zrb/runner/web_route/task_input_api_route.py +5 -5
  53. zrb/runner/web_util/user.py +7 -3
  54. zrb/session/any_session.py +12 -6
  55. zrb/session/session.py +39 -18
  56. zrb/task/any_task.py +24 -3
  57. zrb/task/base/context.py +17 -9
  58. zrb/task/base/execution.py +15 -8
  59. zrb/task/base/lifecycle.py +8 -4
  60. zrb/task/base/monitoring.py +12 -7
  61. zrb/task/base_task.py +69 -5
  62. zrb/task/base_trigger.py +12 -5
  63. zrb/task/llm/agent.py +128 -167
  64. zrb/task/llm/agent_runner.py +152 -0
  65. zrb/task/llm/config.py +39 -20
  66. zrb/task/llm/conversation_history.py +110 -29
  67. zrb/task/llm/conversation_history_model.py +4 -179
  68. zrb/task/llm/default_workflow/coding/workflow.md +41 -0
  69. zrb/task/llm/default_workflow/copywriting/workflow.md +68 -0
  70. zrb/task/llm/default_workflow/git/workflow.md +118 -0
  71. zrb/task/llm/default_workflow/golang/workflow.md +128 -0
  72. zrb/task/llm/default_workflow/html-css/workflow.md +135 -0
  73. zrb/task/llm/default_workflow/java/workflow.md +146 -0
  74. zrb/task/llm/default_workflow/javascript/workflow.md +158 -0
  75. zrb/task/llm/default_workflow/python/workflow.md +160 -0
  76. zrb/task/llm/default_workflow/researching/workflow.md +153 -0
  77. zrb/task/llm/default_workflow/rust/workflow.md +162 -0
  78. zrb/task/llm/default_workflow/shell/workflow.md +299 -0
  79. zrb/task/llm/file_replacement.py +206 -0
  80. zrb/task/llm/file_tool_model.py +57 -0
  81. zrb/task/llm/history_processor.py +206 -0
  82. zrb/task/llm/history_summarization.py +2 -193
  83. zrb/task/llm/print_node.py +184 -64
  84. zrb/task/llm/prompt.py +175 -179
  85. zrb/task/llm/subagent_conversation_history.py +41 -0
  86. zrb/task/llm/tool_wrapper.py +226 -85
  87. zrb/task/llm/workflow.py +76 -0
  88. zrb/task/llm_task.py +109 -71
  89. zrb/task/make_task.py +2 -3
  90. zrb/task/rsync_task.py +25 -10
  91. zrb/task/scheduler.py +4 -4
  92. zrb/util/attr.py +54 -39
  93. zrb/util/cli/markdown.py +12 -0
  94. zrb/util/cli/text.py +30 -0
  95. zrb/util/file.py +12 -3
  96. zrb/util/git.py +2 -2
  97. zrb/util/{llm/prompt.py → markdown.py} +2 -3
  98. zrb/util/string/conversion.py +1 -1
  99. zrb/util/truncate.py +23 -0
  100. zrb/util/yaml.py +204 -0
  101. zrb/xcom/xcom.py +10 -0
  102. {zrb-1.15.3.dist-info → zrb-1.21.29.dist-info}/METADATA +38 -18
  103. {zrb-1.15.3.dist-info → zrb-1.21.29.dist-info}/RECORD +105 -79
  104. {zrb-1.15.3.dist-info → zrb-1.21.29.dist-info}/WHEEL +1 -1
  105. zrb/task/llm/default_workflow/coding.md +0 -24
  106. zrb/task/llm/default_workflow/copywriting.md +0 -17
  107. zrb/task/llm/default_workflow/researching.md +0 -18
  108. {zrb-1.15.3.dist-info → zrb-1.21.29.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,206 @@
1
+ import json
2
+ import sys
3
+ import traceback
4
+ from typing import TYPE_CHECKING, Any, Callable, Coroutine
5
+
6
+ from zrb.config.llm_config import llm_config
7
+ from zrb.config.llm_rate_limitter import LLMRateLimitter
8
+ from zrb.config.llm_rate_limitter import llm_rate_limitter as default_llm_rate_limitter
9
+ from zrb.context.any_context import AnyContext
10
+ from zrb.task.llm.agent_runner import run_agent_iteration
11
+ from zrb.util.cli.style import stylize_faint
12
+ from zrb.util.markdown import make_markdown_section
13
+
14
+ if sys.version_info >= (3, 12):
15
+ from typing import TypedDict
16
+ else:
17
+ from typing_extensions import TypedDict
18
+
19
+
20
+ if TYPE_CHECKING:
21
+ from pydantic_ai import ModelMessage
22
+ from pydantic_ai.models import Model
23
+ from pydantic_ai.settings import ModelSettings
24
+
25
+
26
+ class SingleMessage(TypedDict):
27
+ """
28
+ SingleConversation
29
+
30
+ Attributes:
31
+ role: Either AI, User, Tool Call, or Tool Result
32
+ time: yyyy-mm-ddTHH:MM:SSZ:
33
+ content: The content of the message (summarize if too long)
34
+ """
35
+
36
+ role: str
37
+ time: str
38
+ content: str
39
+
40
+
41
+ class ConversationSummary(TypedDict):
42
+ """
43
+ Conversation history
44
+
45
+ Attributes:
46
+ transcript: Several last transcript of the conversation
47
+ summary: Descriptive conversation summary
48
+ """
49
+
50
+ transcript: list[SingleMessage]
51
+ summary: str
52
+
53
+
54
+ def save_conversation_summary(conversation_summary: ConversationSummary):
55
+ """
56
+ Write conversation summary for main assistant to continue conversation.
57
+ """
58
+ return conversation_summary
59
+
60
+
61
+ def create_summarize_history_processor(
62
+ ctx: AnyContext,
63
+ system_prompt: str,
64
+ rate_limitter: LLMRateLimitter | None = None,
65
+ summarization_model: "Model | str | None" = None,
66
+ summarization_model_settings: "ModelSettings | None" = None,
67
+ summarization_system_prompt: str | None = None,
68
+ summarization_token_threshold: int | None = None,
69
+ summarization_retries: int = 2,
70
+ ) -> Callable[[list["ModelMessage"]], Coroutine[None, None, list["ModelMessage"]]]:
71
+ from pydantic_ai import Agent, ModelMessage, ModelRequest
72
+ from pydantic_ai.messages import ModelMessagesTypeAdapter, UserPromptPart
73
+
74
+ if rate_limitter is None:
75
+ rate_limitter = default_llm_rate_limitter
76
+ if summarization_model is None:
77
+ summarization_model = llm_config.default_small_model
78
+ if summarization_model_settings is None:
79
+ summarization_model_settings = llm_config.default_small_model_settings
80
+ if summarization_system_prompt is None:
81
+ summarization_system_prompt = llm_config.default_summarization_prompt
82
+ if summarization_token_threshold is None:
83
+ summarization_token_threshold = (
84
+ llm_config.default_history_summarization_token_threshold
85
+ )
86
+
87
+ async def maybe_summarize_history(
88
+ messages: list[ModelMessage],
89
+ ) -> list[ModelMessage]:
90
+ history_list = json.loads(ModelMessagesTypeAdapter.dump_json(messages))
91
+ history_json_str = json.dumps(history_list)
92
+ # Estimate token usage
93
+ # Note: Pydantic ai has run context parameter
94
+ # (https://ai.pydantic.dev/message-history/#runcontext-parameter)
95
+ # But we cannot use run_ctx.usage.total_tokens because total token keep increasing
96
+ # even after summariztion.
97
+ estimated_token_usage = rate_limitter.count_token(history_json_str)
98
+ _print_request_info(
99
+ ctx, estimated_token_usage, summarization_token_threshold, messages
100
+ )
101
+ if estimated_token_usage < summarization_token_threshold or len(messages) == 1:
102
+ return messages
103
+ history_list_without_instruction = [
104
+ {
105
+ key: obj[key]
106
+ for key in obj
107
+ if index == len(history_list) - 1 or key != "instructions"
108
+ }
109
+ for index, obj in enumerate(history_list)
110
+ ]
111
+ history_json_str_without_instruction = json.dumps(
112
+ history_list_without_instruction
113
+ )
114
+ summarization_message = f"Summarize the following conversation: {history_json_str_without_instruction}"
115
+ summarization_agent = Agent[None, ConversationSummary](
116
+ model=summarization_model,
117
+ output_type=save_conversation_summary,
118
+ instructions=summarization_system_prompt,
119
+ model_settings=summarization_model_settings,
120
+ retries=summarization_retries,
121
+ )
122
+ try:
123
+ _print_info(ctx, "📝 Rollup Conversation", 2)
124
+ summary_run = await run_agent_iteration(
125
+ ctx=ctx,
126
+ agent=summarization_agent,
127
+ user_prompt=summarization_message,
128
+ attachments=[],
129
+ history_list=[],
130
+ rate_limitter=rate_limitter,
131
+ log_indent_level=2,
132
+ )
133
+ if summary_run and summary_run.result and summary_run.result.output:
134
+ usage = summary_run.result.usage()
135
+ _print_info(ctx, f"📝 Rollup Conversation Token: {usage}", 2)
136
+ ctx.print(plain=True)
137
+ ctx.log_info("History summarized and updated.")
138
+ condensed_message = make_markdown_section(
139
+ header="Past Conversation",
140
+ content="\n".join(
141
+ [
142
+ make_markdown_section(
143
+ "Summary", _extract_summary(summary_run.result.output)
144
+ ),
145
+ make_markdown_section(
146
+ "Past Trancript",
147
+ _extract_transcript(summary_run.result.output),
148
+ ),
149
+ ]
150
+ ),
151
+ )
152
+ return [
153
+ ModelRequest(
154
+ instructions=system_prompt,
155
+ parts=[UserPromptPart(condensed_message)],
156
+ )
157
+ ]
158
+ ctx.log_warning("History summarization failed or returned no data.")
159
+ except BaseException as e:
160
+ ctx.log_warning(f"Error during history summarization: {e}")
161
+ traceback.print_exc()
162
+ return messages
163
+
164
+ return maybe_summarize_history
165
+
166
+
167
+ def _print_request_info(
168
+ ctx: AnyContext,
169
+ estimated_token_usage: int,
170
+ summarization_token_threshold: int,
171
+ messages: list["ModelMessage"],
172
+ ):
173
+ _print_info(ctx, f"Current request token (estimated): {estimated_token_usage}")
174
+ _print_info(ctx, f"Summarization token threshold: {summarization_token_threshold}")
175
+ _print_info(ctx, f"History length: {len(messages)}")
176
+
177
+
178
+ def _print_info(ctx: AnyContext, text: str, log_indent_level: int = 0):
179
+ log_prefix = (2 * (log_indent_level + 1)) * " "
180
+ ctx.print(stylize_faint(f"{log_prefix}{text}"), plain=True)
181
+
182
+
183
+ def _extract_summary(summary_result_output: dict[str, Any] | str) -> str:
184
+ summary = (
185
+ summary_result_output.get("summary", "")
186
+ if isinstance(summary_result_output, dict)
187
+ else ""
188
+ )
189
+ return summary
190
+
191
+
192
+ def _extract_transcript(summary_result_output: dict[str, Any] | str) -> str:
193
+ transcript_list = (
194
+ summary_result_output.get("transcript", [])
195
+ if isinstance(summary_result_output, dict)
196
+ else []
197
+ )
198
+ transcript_list = [] if not isinstance(transcript_list, list) else transcript_list
199
+ return "\n".join(_format_transcript_message(message) for message in transcript_list)
200
+
201
+
202
+ def _format_transcript_message(message: dict[str, str]) -> str:
203
+ role = message.get("role", "Message")
204
+ time = message.get("time", "<unknown>")
205
+ content = message.get("content", "<empty>")
206
+ return f"{role} ({time}): {content}"
@@ -1,31 +1,7 @@
1
- import json
2
- import traceback
3
- from typing import TYPE_CHECKING
4
-
5
- from zrb.attr.type import BoolAttr, IntAttr
1
+ from zrb.attr.type import IntAttr
6
2
  from zrb.config.llm_config import llm_config
7
- from zrb.config.llm_rate_limitter import LLMRateLimiter, llm_rate_limitter
8
3
  from zrb.context.any_context import AnyContext
9
- from zrb.task.llm.agent import run_agent_iteration
10
- from zrb.task.llm.conversation_history import (
11
- count_part_in_history_list,
12
- replace_system_prompt_in_history,
13
- )
14
- from zrb.task.llm.conversation_history_model import ConversationHistory
15
- from zrb.task.llm.typing import ListOfDict
16
- from zrb.util.attr import get_bool_attr, get_int_attr
17
- from zrb.util.cli.style import stylize_faint
18
- from zrb.util.llm.prompt import make_prompt_section
19
-
20
- if TYPE_CHECKING:
21
- from pydantic_ai.models import Model
22
- from pydantic_ai.settings import ModelSettings
23
-
24
-
25
- def _count_token_in_history(history_list: ListOfDict) -> int:
26
- """Counts the total number of tokens in a conversation history list."""
27
- text_to_count = json.dumps(history_list)
28
- return llm_rate_limitter.count_token(text_to_count)
4
+ from zrb.util.attr import get_int_attr
29
5
 
30
6
 
31
7
  def get_history_summarization_token_threshold(
@@ -47,170 +23,3 @@ def get_history_summarization_token_threshold(
47
23
  "Defaulting to -1 (no threshold)."
48
24
  )
49
25
  return -1
50
-
51
-
52
- def should_summarize_history(
53
- ctx: AnyContext,
54
- history_list: ListOfDict,
55
- should_summarize_history_attr: BoolAttr | None,
56
- render_summarize_history: bool,
57
- history_summarization_token_threshold_attr: IntAttr | None,
58
- render_history_summarization_token_threshold: bool,
59
- ) -> bool:
60
- """Determines if history summarization should occur based on token length and config."""
61
- history_part_count = count_part_in_history_list(history_list)
62
- if history_part_count == 0:
63
- return False
64
- summarization_token_threshold = get_history_summarization_token_threshold(
65
- ctx,
66
- history_summarization_token_threshold_attr,
67
- render_history_summarization_token_threshold,
68
- )
69
- history_token_count = _count_token_in_history(history_list)
70
- if (
71
- summarization_token_threshold == -1
72
- or summarization_token_threshold > history_token_count
73
- ):
74
- return False
75
- return get_bool_attr(
76
- ctx,
77
- should_summarize_history_attr,
78
- llm_config.default_summarize_history,
79
- auto_render=render_summarize_history,
80
- )
81
-
82
-
83
- async def summarize_history(
84
- ctx: AnyContext,
85
- model: "Model | str | None",
86
- settings: "ModelSettings | None",
87
- system_prompt: str,
88
- conversation_history: ConversationHistory,
89
- rate_limitter: LLMRateLimiter | None = None,
90
- retries: int = 3,
91
- ) -> str:
92
- """Runs an LLM call to update the conversation summary."""
93
- from pydantic_ai import Agent
94
-
95
- ctx.log_info("Attempting to summarize conversation history...")
96
- # Construct the user prompt for the summarization agent
97
- user_prompt = "\n".join(
98
- [
99
- make_prompt_section(
100
- "Past Conversation",
101
- "\n".join(
102
- [
103
- make_prompt_section(
104
- "Summary",
105
- conversation_history.past_conversation_summary,
106
- as_code=True,
107
- ),
108
- make_prompt_section(
109
- "Last Transcript",
110
- conversation_history.past_conversation_transcript,
111
- as_code=True,
112
- ),
113
- ]
114
- ),
115
- ),
116
- make_prompt_section(
117
- "Recent Conversation (JSON)",
118
- json.dumps(conversation_history.history),
119
- as_code=True,
120
- ),
121
- make_prompt_section(
122
- "Notes",
123
- "\n".join(
124
- [
125
- make_prompt_section(
126
- "Long Term",
127
- conversation_history.long_term_note,
128
- as_code=True,
129
- ),
130
- make_prompt_section(
131
- "Contextual",
132
- conversation_history.contextual_note,
133
- as_code=True,
134
- ),
135
- ]
136
- ),
137
- ),
138
- ]
139
- )
140
- summarization_agent = Agent(
141
- model=model,
142
- system_prompt=system_prompt,
143
- model_settings=settings,
144
- retries=retries,
145
- tools=[
146
- conversation_history.write_past_conversation_summary,
147
- conversation_history.write_past_conversation_transcript,
148
- conversation_history.read_long_term_note,
149
- conversation_history.write_long_term_note,
150
- conversation_history.read_contextual_note,
151
- conversation_history.write_contextual_note,
152
- ],
153
- )
154
- try:
155
- ctx.print(stylize_faint("📝 Summarize Conversation >>>"), plain=True)
156
- summary_run = await run_agent_iteration(
157
- ctx=ctx,
158
- agent=summarization_agent,
159
- user_prompt=user_prompt,
160
- attachments=[],
161
- history_list=[],
162
- rate_limitter=rate_limitter,
163
- )
164
- if summary_run and summary_run.result and summary_run.result.output:
165
- usage = summary_run.result.usage()
166
- ctx.print(stylize_faint(f"📝 Summarization Token: {usage}"), plain=True)
167
- ctx.print(plain=True)
168
- ctx.log_info("History summarized and updated.")
169
- else:
170
- ctx.log_warning("History summarization failed or returned no data.")
171
- except BaseException as e:
172
- ctx.log_warning(f"Error during history summarization: {e}")
173
- traceback.print_exc()
174
- # Return the original summary if summarization fails
175
- return conversation_history
176
-
177
-
178
- async def maybe_summarize_history(
179
- ctx: AnyContext,
180
- conversation_history: ConversationHistory,
181
- should_summarize_history_attr: BoolAttr | None,
182
- render_summarize_history: bool,
183
- history_summarization_token_threshold_attr: IntAttr | None,
184
- render_history_summarization_token_threshold: bool,
185
- model: "str | Model | None",
186
- model_settings: "ModelSettings | None",
187
- summarization_prompt: str,
188
- rate_limitter: LLMRateLimiter | None = None,
189
- ) -> ConversationHistory:
190
- """Summarizes history and updates context if enabled and threshold met."""
191
- shorten_history = replace_system_prompt_in_history(conversation_history.history)
192
- if should_summarize_history(
193
- ctx,
194
- shorten_history,
195
- should_summarize_history_attr,
196
- render_summarize_history,
197
- history_summarization_token_threshold_attr,
198
- render_history_summarization_token_threshold,
199
- ):
200
- original_history = conversation_history.history
201
- conversation_history.history = shorten_history
202
- conversation_history = await summarize_history(
203
- ctx=ctx,
204
- model=model,
205
- settings=model_settings,
206
- system_prompt=summarization_prompt,
207
- conversation_history=conversation_history,
208
- rate_limitter=rate_limitter,
209
- )
210
- conversation_history.history = original_history
211
- if (
212
- conversation_history.past_conversation_summary != ""
213
- and conversation_history.past_conversation_transcript != ""
214
- ):
215
- conversation_history.history = []
216
- return conversation_history
@@ -1,12 +1,17 @@
1
+ import json
1
2
  from collections.abc import Callable
2
3
  from typing import Any
3
4
 
5
+ from zrb.config.config import CFG
4
6
  from zrb.util.cli.style import stylize_faint
5
7
 
6
8
 
7
- async def print_node(print_func: Callable, agent_run: Any, node: Any):
9
+ async def print_node(
10
+ print_func: Callable, agent_run: Any, node: Any, log_indent_level: int = 0
11
+ ):
8
12
  """Prints the details of an agent execution node using a provided print function."""
9
13
  from pydantic_ai import Agent
14
+ from pydantic_ai.exceptions import UnexpectedModelBehavior
10
15
  from pydantic_ai.messages import (
11
16
  FinalResultEvent,
12
17
  FunctionToolCallEvent,
@@ -18,79 +23,194 @@ async def print_node(print_func: Callable, agent_run: Any, node: Any):
18
23
  ToolCallPartDelta,
19
24
  )
20
25
 
26
+ meta = getattr(node, "id", None) or getattr(node, "request_id", None)
21
27
  if Agent.is_user_prompt_node(node):
22
- print_func(stylize_faint(" 🔠 Receiving input..."))
28
+ print_func(_format_header("🔠 Receiving input...", log_indent_level))
23
29
  elif Agent.is_model_request_node(node):
24
30
  # A model request node => We can stream tokens from the model's request
25
- print_func(stylize_faint(" 🧠 Processing..."))
26
- # Reference: https://ai.pydantic.dev/agents/#streaming
27
- async with node.stream(agent_run.ctx) as request_stream:
28
- is_streaming = False
29
- async for event in request_stream:
30
- if isinstance(event, PartStartEvent) and event.part:
31
- if is_streaming:
32
- print_func("")
33
- content = _get_event_part_content(event)
34
- print_func(stylize_faint(f" {content}"), end="")
35
- is_streaming = False
36
- elif isinstance(event, PartDeltaEvent):
37
- if isinstance(event.delta, TextPartDelta) or isinstance(
38
- event.delta, ThinkingPartDelta
39
- ):
40
- print_func(
41
- stylize_faint(f"{event.delta.content_delta}"),
42
- end="",
43
- )
44
- elif isinstance(event.delta, ToolCallPartDelta):
31
+ print_func(_format_header("🧠 Processing...", log_indent_level))
32
+ # Reference: https://ai.pydantic.dev/agents/#streaming-all-events-and-output
33
+ try:
34
+ async with node.stream(agent_run.ctx) as request_stream:
35
+ is_streaming = False
36
+ async for event in request_stream:
37
+ if isinstance(event, PartStartEvent) and event.part:
38
+ if is_streaming:
39
+ print_func("")
40
+ content = _get_event_part_content(event)
41
+ print_func(_format_content(content, log_indent_level), end="")
42
+ is_streaming = True
43
+ elif isinstance(event, PartDeltaEvent):
44
+ if isinstance(event.delta, TextPartDelta):
45
+ content_delta = event.delta.content_delta
46
+ print_func(
47
+ _format_stream_content(content_delta, log_indent_level),
48
+ end="",
49
+ )
50
+ elif isinstance(event.delta, ThinkingPartDelta):
51
+ content_delta = event.delta.content_delta
52
+ print_func(
53
+ _format_stream_content(content_delta, log_indent_level),
54
+ end="",
55
+ )
56
+ elif isinstance(event.delta, ToolCallPartDelta):
57
+ args_delta = event.delta.args_delta
58
+ if isinstance(args_delta, dict):
59
+ args_delta = json.dumps(args_delta)
60
+ print_func(
61
+ _format_stream_content(args_delta, log_indent_level),
62
+ end="",
63
+ )
64
+ is_streaming = True
65
+ elif isinstance(event, FinalResultEvent) and event.tool_name:
66
+ if is_streaming:
67
+ print_func("")
68
+ tool_name = event.tool_name
45
69
  print_func(
46
- stylize_faint(f"{event.delta.args_delta}"),
47
- end="",
70
+ _format_content(
71
+ f"Result: tool_name={tool_name}", log_indent_level
72
+ )
48
73
  )
49
- is_streaming = True
50
- elif isinstance(event, FinalResultEvent) and event.tool_name:
51
- if is_streaming:
52
- print_func("")
53
- print_func(
54
- stylize_faint(f" Result: tool_name={event.tool_name}"),
55
- )
56
- is_streaming = False
57
- if is_streaming:
58
- print_func("")
74
+ is_streaming = False
75
+ if is_streaming:
76
+ print_func("")
77
+ except UnexpectedModelBehavior as e:
78
+ print_func("") # ensure newline consistency
79
+ print_func(
80
+ _format_content(
81
+ (
82
+ f"🟡 Unexpected Model Behavior: {e}. "
83
+ f"Cause: {e.__cause__}. Node.Id: {meta}"
84
+ ),
85
+ log_indent_level,
86
+ )
87
+ )
59
88
  elif Agent.is_call_tools_node(node):
60
89
  # A handle-response node => The model returned some data, potentially calls a tool
61
- print_func(stylize_faint(" 🧰 Calling Tool..."))
62
- async with node.stream(agent_run.ctx) as handle_stream:
63
- async for event in handle_stream:
64
- if isinstance(event, FunctionToolCallEvent):
65
- # Handle empty arguments across different providers
66
- if event.part.args == "" or event.part.args is None:
67
- event.part.args = {}
68
- elif isinstance(
69
- event.part.args, str
70
- ) and event.part.args.strip() in ["null", "{}"]:
71
- # Some providers might send "null" or "{}" as a string
72
- event.part.args = {}
73
- # Handle dummy property if present (from our schema sanitization)
74
- if (
75
- isinstance(event.part.args, dict)
76
- and "_dummy" in event.part.args
77
- ):
78
- del event.part.args["_dummy"]
79
- print_func(
80
- stylize_faint(
81
- f" {event.part.tool_call_id} | "
82
- f"Call {event.part.tool_name} {event.part.args}"
83
- )
84
- )
85
- elif isinstance(event, FunctionToolResultEvent):
86
- print_func(
87
- stylize_faint(
88
- f" {event.tool_call_id} | {event.result.content}"
90
+ print_func(_format_header("🧰 Calling Tool...", log_indent_level))
91
+ try:
92
+ async with node.stream(agent_run.ctx) as handle_stream:
93
+ async for event in handle_stream:
94
+ if isinstance(event, FunctionToolCallEvent):
95
+ args = _get_event_part_args(event)
96
+ call_id = event.part.tool_call_id
97
+ tool_name = event.part.tool_name
98
+ print_func(
99
+ _format_content(
100
+ f"{call_id} | Call {tool_name} {args}", log_indent_level
101
+ )
89
102
  )
90
- )
103
+ elif (
104
+ isinstance(event, FunctionToolResultEvent)
105
+ and event.tool_call_id
106
+ ):
107
+ call_id = event.tool_call_id
108
+ if CFG.LLM_SHOW_TOOL_CALL_RESULT:
109
+ result_content = event.result.content
110
+ print_func(
111
+ _format_content(
112
+ f"{call_id} | Return {result_content}",
113
+ log_indent_level,
114
+ )
115
+ )
116
+ else:
117
+ print_func(
118
+ _format_content(
119
+ f"{call_id} | Executed", log_indent_level
120
+ )
121
+ )
122
+ except UnexpectedModelBehavior as e:
123
+ print_func("") # ensure newline consistency
124
+ print_func(
125
+ _format_content(
126
+ (
127
+ f"🟡 Unexpected Model Behavior: {e}. "
128
+ f"Cause: {e.__cause__}. Node.Id: {meta}"
129
+ ),
130
+ log_indent_level,
131
+ )
132
+ )
91
133
  elif Agent.is_end_node(node):
92
134
  # Once an End node is reached, the agent run is complete
93
- print_func(stylize_faint(" ✅ Completed..."))
135
+ print_func(_format_header("✅ Completed...", log_indent_level))
136
+
137
+
138
+ def _format_header(text: str | None, log_indent_level: int = 0) -> str:
139
+ return _format(
140
+ text,
141
+ base_indent=2,
142
+ first_indent=0,
143
+ indent=0,
144
+ log_indent_level=log_indent_level,
145
+ )
146
+
147
+
148
+ def _format_content(text: str | None, log_indent_level: int = 0) -> str:
149
+ return _format(
150
+ text,
151
+ base_indent=2,
152
+ first_indent=3,
153
+ indent=3,
154
+ log_indent_level=log_indent_level,
155
+ )
156
+
157
+
158
+ def _format_stream_content(text: str | None, log_indent_level: int = 0) -> str:
159
+ return _format(
160
+ text,
161
+ base_indent=2,
162
+ indent=3,
163
+ log_indent_level=log_indent_level,
164
+ is_stream=True,
165
+ )
166
+
167
+
168
+ def _format(
169
+ text: str | None,
170
+ base_indent: int = 0,
171
+ first_indent: int = 0,
172
+ indent: int = 0,
173
+ log_indent_level: int = 0,
174
+ is_stream: bool = False,
175
+ ) -> str:
176
+ if text is None:
177
+ text = ""
178
+ line_prefix = (base_indent * (log_indent_level + 1) + indent) * " "
179
+ processed_text = text.replace("\n", f"\n{line_prefix}")
180
+ if is_stream:
181
+ return stylize_faint(processed_text)
182
+ first_line_prefix = (base_indent * (log_indent_level + 1) + first_indent) * " "
183
+ return stylize_faint(f"{first_line_prefix}{processed_text}")
184
+
185
+
186
+ def _get_event_part_args(event: Any) -> Any:
187
+ # Handle empty arguments across different providers
188
+ if event.part.args == "" or event.part.args is None:
189
+ return {}
190
+ if isinstance(event.part.args, str):
191
+ # Some providers might send "null" or "{}" as a string
192
+ if event.part.args.strip() in ["null", "{}"]:
193
+ return {}
194
+ try:
195
+ obj = json.loads(event.part.args)
196
+ if isinstance(obj, dict):
197
+ return _truncate_kwargs(obj)
198
+ except json.JSONDecodeError:
199
+ pass
200
+ # Handle dummy property if present (from our schema sanitization)
201
+ if isinstance(event.part.args, dict):
202
+ return _truncate_kwargs(event.part.args)
203
+ return event.part.args
204
+
205
+
206
+ def _truncate_kwargs(kwargs: dict[str, Any]) -> dict[str, Any]:
207
+ return {key: _truncate_arg(val) for key, val in kwargs.items()}
208
+
209
+
210
+ def _truncate_arg(arg: str, length: int = 19) -> str:
211
+ if isinstance(arg, str) and len(arg) > length:
212
+ return f"{arg[:length-4]} ..."
213
+ return arg
94
214
 
95
215
 
96
216
  def _get_event_part_content(event: Any) -> str: