zrb 1.8.10__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 (147) hide show
  1. zrb/__init__.py +126 -113
  2. zrb/__main__.py +1 -1
  3. zrb/attr/type.py +10 -7
  4. zrb/builtin/__init__.py +2 -50
  5. zrb/builtin/git.py +12 -1
  6. zrb/builtin/group.py +31 -15
  7. zrb/builtin/http.py +7 -8
  8. zrb/builtin/llm/attachment.py +40 -0
  9. zrb/builtin/llm/chat_completion.py +274 -0
  10. zrb/builtin/llm/chat_session.py +152 -85
  11. zrb/builtin/llm/chat_session_cmd.py +288 -0
  12. zrb/builtin/llm/chat_trigger.py +79 -0
  13. zrb/builtin/llm/history.py +7 -9
  14. zrb/builtin/llm/llm_ask.py +221 -98
  15. zrb/builtin/llm/tool/api.py +74 -52
  16. zrb/builtin/llm/tool/cli.py +46 -17
  17. zrb/builtin/llm/tool/code.py +71 -90
  18. zrb/builtin/llm/tool/file.py +301 -241
  19. zrb/builtin/llm/tool/note.py +84 -0
  20. zrb/builtin/llm/tool/rag.py +38 -8
  21. zrb/builtin/llm/tool/sub_agent.py +67 -50
  22. zrb/builtin/llm/tool/web.py +146 -122
  23. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +7 -7
  24. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +5 -5
  25. zrb/builtin/project/add/fastapp/fastapp_util.py +1 -1
  26. zrb/builtin/searxng/config/settings.yml +5671 -0
  27. zrb/builtin/searxng/start.py +21 -0
  28. zrb/builtin/setup/latex/ubuntu.py +1 -0
  29. zrb/builtin/setup/ubuntu.py +1 -1
  30. zrb/builtin/shell/autocomplete/bash.py +4 -3
  31. zrb/builtin/shell/autocomplete/zsh.py +4 -3
  32. zrb/builtin/todo.py +13 -2
  33. zrb/config/config.py +614 -0
  34. zrb/config/default_prompt/file_extractor_system_prompt.md +112 -0
  35. zrb/config/default_prompt/interactive_system_prompt.md +29 -0
  36. zrb/config/default_prompt/persona.md +1 -0
  37. zrb/config/default_prompt/repo_extractor_system_prompt.md +112 -0
  38. zrb/config/default_prompt/repo_summarizer_system_prompt.md +29 -0
  39. zrb/config/default_prompt/summarization_prompt.md +57 -0
  40. zrb/config/default_prompt/system_prompt.md +38 -0
  41. zrb/config/llm_config.py +339 -0
  42. zrb/config/llm_context/config.py +166 -0
  43. zrb/config/llm_context/config_parser.py +40 -0
  44. zrb/config/llm_context/workflow.py +81 -0
  45. zrb/config/llm_rate_limitter.py +190 -0
  46. zrb/{runner → config}/web_auth_config.py +17 -22
  47. zrb/context/any_shared_context.py +17 -1
  48. zrb/context/context.py +16 -2
  49. zrb/context/shared_context.py +18 -8
  50. zrb/group/any_group.py +12 -5
  51. zrb/group/group.py +67 -3
  52. zrb/input/any_input.py +5 -1
  53. zrb/input/base_input.py +18 -6
  54. zrb/input/option_input.py +13 -1
  55. zrb/input/text_input.py +8 -25
  56. zrb/runner/cli.py +25 -23
  57. zrb/runner/common_util.py +24 -19
  58. zrb/runner/web_app.py +3 -3
  59. zrb/runner/web_route/docs_route.py +1 -1
  60. zrb/runner/web_route/error_page/serve_default_404.py +1 -1
  61. zrb/runner/web_route/error_page/show_error_page.py +1 -1
  62. zrb/runner/web_route/home_page/home_page_route.py +2 -2
  63. zrb/runner/web_route/login_api_route.py +1 -1
  64. zrb/runner/web_route/login_page/login_page_route.py +2 -2
  65. zrb/runner/web_route/logout_api_route.py +1 -1
  66. zrb/runner/web_route/logout_page/logout_page_route.py +2 -2
  67. zrb/runner/web_route/node_page/group/show_group_page.py +1 -1
  68. zrb/runner/web_route/node_page/node_page_route.py +1 -1
  69. zrb/runner/web_route/node_page/task/show_task_page.py +1 -1
  70. zrb/runner/web_route/refresh_token_api_route.py +1 -1
  71. zrb/runner/web_route/static/static_route.py +1 -1
  72. zrb/runner/web_route/task_input_api_route.py +6 -6
  73. zrb/runner/web_route/task_session_api_route.py +20 -12
  74. zrb/runner/web_util/cookie.py +1 -1
  75. zrb/runner/web_util/token.py +1 -1
  76. zrb/runner/web_util/user.py +8 -4
  77. zrb/session/any_session.py +24 -17
  78. zrb/session/session.py +50 -25
  79. zrb/session_state_logger/any_session_state_logger.py +9 -4
  80. zrb/session_state_logger/file_session_state_logger.py +16 -6
  81. zrb/session_state_logger/session_state_logger_factory.py +1 -1
  82. zrb/task/any_task.py +30 -9
  83. zrb/task/base/context.py +17 -9
  84. zrb/task/base/execution.py +15 -8
  85. zrb/task/base/lifecycle.py +8 -4
  86. zrb/task/base/monitoring.py +12 -7
  87. zrb/task/base_task.py +69 -5
  88. zrb/task/base_trigger.py +12 -5
  89. zrb/task/cmd_task.py +1 -1
  90. zrb/task/llm/agent.py +154 -161
  91. zrb/task/llm/agent_runner.py +152 -0
  92. zrb/task/llm/config.py +47 -18
  93. zrb/task/llm/conversation_history.py +209 -0
  94. zrb/task/llm/conversation_history_model.py +67 -0
  95. zrb/task/llm/default_workflow/coding/workflow.md +41 -0
  96. zrb/task/llm/default_workflow/copywriting/workflow.md +68 -0
  97. zrb/task/llm/default_workflow/git/workflow.md +118 -0
  98. zrb/task/llm/default_workflow/golang/workflow.md +128 -0
  99. zrb/task/llm/default_workflow/html-css/workflow.md +135 -0
  100. zrb/task/llm/default_workflow/java/workflow.md +146 -0
  101. zrb/task/llm/default_workflow/javascript/workflow.md +158 -0
  102. zrb/task/llm/default_workflow/python/workflow.md +160 -0
  103. zrb/task/llm/default_workflow/researching/workflow.md +153 -0
  104. zrb/task/llm/default_workflow/rust/workflow.md +162 -0
  105. zrb/task/llm/default_workflow/shell/workflow.md +299 -0
  106. zrb/task/llm/error.py +24 -10
  107. zrb/task/llm/file_replacement.py +206 -0
  108. zrb/task/llm/file_tool_model.py +57 -0
  109. zrb/task/llm/history_processor.py +206 -0
  110. zrb/task/llm/history_summarization.py +11 -166
  111. zrb/task/llm/print_node.py +193 -69
  112. zrb/task/llm/prompt.py +242 -45
  113. zrb/task/llm/subagent_conversation_history.py +41 -0
  114. zrb/task/llm/tool_wrapper.py +260 -57
  115. zrb/task/llm/workflow.py +76 -0
  116. zrb/task/llm_task.py +182 -171
  117. zrb/task/make_task.py +2 -3
  118. zrb/task/rsync_task.py +26 -11
  119. zrb/task/scheduler.py +4 -4
  120. zrb/util/attr.py +54 -39
  121. zrb/util/callable.py +23 -0
  122. zrb/util/cli/markdown.py +12 -0
  123. zrb/util/cli/text.py +30 -0
  124. zrb/util/file.py +29 -11
  125. zrb/util/git.py +8 -11
  126. zrb/util/git_diff_model.py +10 -0
  127. zrb/util/git_subtree.py +9 -14
  128. zrb/util/git_subtree_model.py +32 -0
  129. zrb/util/init_path.py +1 -1
  130. zrb/util/markdown.py +62 -0
  131. zrb/util/string/conversion.py +2 -2
  132. zrb/util/todo.py +17 -50
  133. zrb/util/todo_model.py +46 -0
  134. zrb/util/truncate.py +23 -0
  135. zrb/util/yaml.py +204 -0
  136. zrb/xcom/xcom.py +10 -0
  137. zrb-1.21.29.dist-info/METADATA +270 -0
  138. {zrb-1.8.10.dist-info → zrb-1.21.29.dist-info}/RECORD +140 -98
  139. {zrb-1.8.10.dist-info → zrb-1.21.29.dist-info}/WHEEL +1 -1
  140. zrb/config.py +0 -335
  141. zrb/llm_config.py +0 -411
  142. zrb/llm_rate_limitter.py +0 -125
  143. zrb/task/llm/context.py +0 -102
  144. zrb/task/llm/context_enrichment.py +0 -199
  145. zrb/task/llm/history.py +0 -211
  146. zrb-1.8.10.dist-info/METADATA +0 -264
  147. {zrb-1.8.10.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,180 +1,25 @@
1
- import json
2
- from typing import TYPE_CHECKING, Any
3
-
4
- from pydantic import BaseModel
5
-
6
- from zrb.attr.type import BoolAttr, IntAttr
1
+ from zrb.attr.type import IntAttr
2
+ from zrb.config.llm_config import llm_config
7
3
  from zrb.context.any_context import AnyContext
8
- from zrb.llm_config import llm_config
9
- from zrb.llm_rate_limitter import LLMRateLimiter
10
- from zrb.task.llm.agent import run_agent_iteration
11
- from zrb.task.llm.history import (
12
- count_part_in_history_list,
13
- replace_system_prompt_in_history_list,
14
- )
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
-
19
- if TYPE_CHECKING:
20
- from pydantic_ai.models import Model
21
- from pydantic_ai.settings import ModelSettings
22
- else:
23
- Model = Any
24
- ModelSettings = Any
4
+ from zrb.util.attr import get_int_attr
25
5
 
26
6
 
27
- def get_history_summarization_threshold(
7
+ def get_history_summarization_token_threshold(
28
8
  ctx: AnyContext,
29
- history_summarization_threshold_attr: IntAttr | None,
30
- render_history_summarization_threshold: bool,
9
+ history_summarization_token_threshold_attr: IntAttr | None,
10
+ render_history_summarization_token_threshold: bool,
31
11
  ) -> int:
32
- """Gets the history summarization threshold, handling defaults and errors."""
12
+ """Gets the history summarization token threshold, handling defaults and errors."""
33
13
  try:
34
14
  return get_int_attr(
35
15
  ctx,
36
- history_summarization_threshold_attr,
37
- # Use llm_config default if attribute is None
38
- llm_config.default_history_summarization_threshold,
39
- auto_render=render_history_summarization_threshold,
16
+ history_summarization_token_threshold_attr,
17
+ llm_config.default_history_summarization_token_threshold,
18
+ auto_render=render_history_summarization_token_threshold,
40
19
  )
41
20
  except ValueError as e:
42
21
  ctx.log_warning(
43
- f"Could not convert history_summarization_threshold to int: {e}. "
22
+ f"Could not convert history_summarization_token_threshold to int: {e}. "
44
23
  "Defaulting to -1 (no threshold)."
45
24
  )
46
25
  return -1
47
-
48
-
49
- def should_summarize_history(
50
- ctx: AnyContext,
51
- history_list: ListOfDict,
52
- should_summarize_history_attr: BoolAttr | None, # Allow None
53
- render_summarize_history: bool,
54
- history_summarization_threshold_attr: IntAttr | None, # Allow None
55
- render_history_summarization_threshold: bool,
56
- ) -> bool:
57
- """Determines if history summarization should occur based on length and config."""
58
- history_part_count = count_part_in_history_list(history_list)
59
- if history_part_count == 0:
60
- return False
61
- summarization_threshold = get_history_summarization_threshold(
62
- ctx,
63
- history_summarization_threshold_attr,
64
- render_history_summarization_threshold,
65
- )
66
- if summarization_threshold == -1 or summarization_threshold > history_part_count:
67
- return False
68
- return get_bool_attr(
69
- ctx,
70
- should_summarize_history_attr,
71
- # Use llm_config default if attribute is None
72
- llm_config.default_summarize_history,
73
- auto_render=render_summarize_history,
74
- )
75
-
76
-
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
- async def summarize_history(
86
- ctx: AnyContext,
87
- config: SummarizationConfig,
88
- conversation_context: dict[str, Any],
89
- history_list: ListOfDict,
90
- rate_limitter: LLMRateLimiter | None = None,
91
- ) -> dict[str, Any]:
92
- """Runs an LLM call to summarize history and update the context."""
93
- from pydantic_ai import Agent
94
-
95
- ctx.log_info("Attempting to summarize conversation history...")
96
-
97
- summarization_agent = Agent(
98
- model=config.model,
99
- system_prompt=config.prompt,
100
- model_settings=config.settings,
101
- retries=config.retries,
102
- )
103
-
104
- # Prepare context and history for summarization prompt
105
- 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)
120
- summary_run = await run_agent_iteration(
121
- ctx=ctx,
122
- agent=summarization_agent,
123
- user_prompt=summarization_user_prompt,
124
- history_list=[], # Summarization agent doesn't need prior history
125
- rate_limitter=rate_limitter,
126
- )
127
- if summary_run and summary_run.result.output:
128
- summary_text = str(summary_run.result.output)
129
- 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}")
135
- else:
136
- ctx.log_warning("History summarization failed or returned no data.")
137
- except Exception as e:
138
- ctx.log_warning(f"Error during history summarization: {e}")
139
-
140
- return conversation_context
141
-
142
-
143
- async def maybe_summarize_history(
144
- ctx: AnyContext,
145
- history_list: ListOfDict,
146
- conversation_context: dict[str, Any],
147
- should_summarize_history_attr: BoolAttr | None, # Allow None
148
- render_summarize_history: bool,
149
- history_summarization_threshold_attr: IntAttr | None, # Allow None
150
- render_history_summarization_threshold: bool,
151
- model: str | Model | None,
152
- model_settings: ModelSettings | None,
153
- summarization_prompt: str,
154
- rate_limitter: LLMRateLimiter | None = None,
155
- ) -> tuple[ListOfDict, dict[str, Any]]:
156
- """Summarizes history and updates context if enabled and threshold met."""
157
- shorten_history_list = replace_system_prompt_in_history_list(history_list)
158
- if should_summarize_history(
159
- ctx,
160
- shorten_history_list,
161
- should_summarize_history_attr,
162
- render_summarize_history,
163
- history_summarization_threshold_attr,
164
- render_history_summarization_threshold,
165
- ):
166
- # Use summarize_history defined above
167
- updated_context = await summarize_history(
168
- 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
176
- rate_limitter=rate_limitter,
177
- )
178
- # Truncate the history list after summarization
179
- return [], updated_context
180
- return history_list, conversation_context
@@ -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,
@@ -14,84 +19,203 @@ async def print_node(print_func: Callable, agent_run: Any, node: Any):
14
19
  PartDeltaEvent,
15
20
  PartStartEvent,
16
21
  TextPartDelta,
22
+ ThinkingPartDelta,
17
23
  ToolCallPartDelta,
18
24
  )
19
25
 
26
+ meta = getattr(node, "id", None) or getattr(node, "request_id", None)
20
27
  if Agent.is_user_prompt_node(node):
21
- # A user prompt node => The user has provided input
22
- print_func(stylize_faint(f">> UserPromptNode: {node.user_prompt}"))
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(
26
- stylize_faint(">> ModelRequestNode: streaming partial request tokens")
27
- )
28
- async with node.stream(agent_run.ctx) as request_stream:
29
- is_streaming = False
30
- async for event in request_stream:
31
- if isinstance(event, PartStartEvent):
32
- if is_streaming:
33
- print_func("")
34
- print_func(
35
- stylize_faint(
36
- f"[Request] Starting part {event.index}: {event.part!r}"
37
- ),
38
- )
39
- is_streaming = False
40
- elif isinstance(event, PartDeltaEvent):
41
- if isinstance(event.delta, TextPartDelta):
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
42
69
  print_func(
43
- stylize_faint(f"{event.delta.content_delta}"),
44
- end="",
70
+ _format_content(
71
+ f"Result: tool_name={tool_name}", log_indent_level
72
+ )
45
73
  )
46
- elif isinstance(event.delta, ToolCallPartDelta):
47
- print_func(
48
- stylize_faint(f"{event.delta.args_delta}"),
49
- end="",
50
- )
51
- is_streaming = True
52
- elif isinstance(event, FinalResultEvent):
53
- if is_streaming:
54
- print_func("")
55
- print_func(
56
- stylize_faint(f"[Result] tool_name={event.tool_name}"),
57
- )
58
- is_streaming = False
59
- if is_streaming:
60
- 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
+ )
61
88
  elif Agent.is_call_tools_node(node):
62
89
  # A handle-response node => The model returned some data, potentially calls a tool
63
- print_func(
64
- stylize_faint(">> CallToolsNode: streaming partial response & tool usage")
65
- )
66
- async with node.stream(agent_run.ctx) as handle_stream:
67
- async for event in handle_stream:
68
- if isinstance(event, FunctionToolCallEvent):
69
- # Handle empty arguments across different providers
70
- if event.part.args == "" or event.part.args is None:
71
- event.part.args = {}
72
- elif isinstance(
73
- event.part.args, str
74
- ) and event.part.args.strip() in ["null", "{}"]:
75
- # Some providers might send "null" or "{}" as a string
76
- event.part.args = {}
77
- # Handle dummy property if present (from our schema sanitization)
78
- if (
79
- isinstance(event.part.args, dict)
80
- and "_dummy" in event.part.args
81
- ):
82
- del event.part.args["_dummy"]
83
- print_func(
84
- 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
86
- )
87
- )
88
- elif isinstance(event, FunctionToolResultEvent):
89
- print_func(
90
- stylize_faint(
91
- f"[Tools] Tool call {event.tool_call_id!r} returned => {event.result.content}" # noqa
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
+ )
92
102
  )
93
- )
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
+ )
94
133
  elif Agent.is_end_node(node):
95
134
  # Once an End node is reached, the agent run is complete
96
- print_func(stylize_faint("[End of Response]"))
97
- # print_func(stylize_faint(f"{agent_run.result.data}"))
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
214
+
215
+
216
+ def _get_event_part_content(event: Any) -> str:
217
+ if not hasattr(event, "part"):
218
+ return f"{event}"
219
+ if not hasattr(event.part, "content"):
220
+ return f"{event.part}"
221
+ return getattr(event.part, "content")