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.
- zrb/__init__.py +2 -6
- zrb/attr/type.py +10 -7
- zrb/builtin/__init__.py +2 -0
- zrb/builtin/git.py +12 -1
- zrb/builtin/group.py +31 -15
- zrb/builtin/llm/attachment.py +40 -0
- zrb/builtin/llm/chat_completion.py +274 -0
- zrb/builtin/llm/chat_session.py +126 -167
- zrb/builtin/llm/chat_session_cmd.py +288 -0
- zrb/builtin/llm/chat_trigger.py +79 -0
- zrb/builtin/llm/history.py +4 -4
- zrb/builtin/llm/llm_ask.py +217 -135
- zrb/builtin/llm/tool/api.py +74 -70
- zrb/builtin/llm/tool/cli.py +35 -21
- zrb/builtin/llm/tool/code.py +55 -73
- zrb/builtin/llm/tool/file.py +278 -344
- zrb/builtin/llm/tool/note.py +84 -0
- zrb/builtin/llm/tool/rag.py +27 -34
- zrb/builtin/llm/tool/sub_agent.py +54 -41
- zrb/builtin/llm/tool/web.py +74 -98
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +7 -7
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +5 -5
- zrb/builtin/project/add/fastapp/fastapp_util.py +1 -1
- zrb/builtin/searxng/config/settings.yml +5671 -0
- zrb/builtin/searxng/start.py +21 -0
- zrb/builtin/shell/autocomplete/bash.py +4 -3
- zrb/builtin/shell/autocomplete/zsh.py +4 -3
- zrb/config/config.py +202 -27
- zrb/config/default_prompt/file_extractor_system_prompt.md +109 -9
- zrb/config/default_prompt/interactive_system_prompt.md +24 -30
- zrb/config/default_prompt/persona.md +1 -1
- zrb/config/default_prompt/repo_extractor_system_prompt.md +31 -31
- zrb/config/default_prompt/repo_summarizer_system_prompt.md +27 -8
- zrb/config/default_prompt/summarization_prompt.md +57 -16
- zrb/config/default_prompt/system_prompt.md +36 -30
- zrb/config/llm_config.py +119 -23
- zrb/config/llm_context/config.py +127 -90
- zrb/config/llm_context/config_parser.py +1 -7
- zrb/config/llm_context/workflow.py +81 -0
- zrb/config/llm_rate_limitter.py +100 -47
- zrb/context/any_shared_context.py +7 -1
- zrb/context/context.py +8 -2
- zrb/context/shared_context.py +3 -7
- zrb/group/any_group.py +3 -3
- zrb/group/group.py +3 -3
- zrb/input/any_input.py +5 -1
- zrb/input/base_input.py +18 -6
- zrb/input/option_input.py +13 -1
- zrb/input/text_input.py +7 -24
- zrb/runner/cli.py +21 -20
- zrb/runner/common_util.py +24 -19
- zrb/runner/web_route/task_input_api_route.py +5 -5
- zrb/runner/web_util/user.py +7 -3
- zrb/session/any_session.py +12 -6
- zrb/session/session.py +39 -18
- zrb/task/any_task.py +24 -3
- zrb/task/base/context.py +17 -9
- zrb/task/base/execution.py +15 -8
- zrb/task/base/lifecycle.py +8 -4
- zrb/task/base/monitoring.py +12 -7
- zrb/task/base_task.py +69 -5
- zrb/task/base_trigger.py +12 -5
- zrb/task/llm/agent.py +128 -167
- zrb/task/llm/agent_runner.py +152 -0
- zrb/task/llm/config.py +39 -20
- zrb/task/llm/conversation_history.py +110 -29
- zrb/task/llm/conversation_history_model.py +4 -179
- zrb/task/llm/default_workflow/coding/workflow.md +41 -0
- zrb/task/llm/default_workflow/copywriting/workflow.md +68 -0
- zrb/task/llm/default_workflow/git/workflow.md +118 -0
- zrb/task/llm/default_workflow/golang/workflow.md +128 -0
- zrb/task/llm/default_workflow/html-css/workflow.md +135 -0
- zrb/task/llm/default_workflow/java/workflow.md +146 -0
- zrb/task/llm/default_workflow/javascript/workflow.md +158 -0
- zrb/task/llm/default_workflow/python/workflow.md +160 -0
- zrb/task/llm/default_workflow/researching/workflow.md +153 -0
- zrb/task/llm/default_workflow/rust/workflow.md +162 -0
- zrb/task/llm/default_workflow/shell/workflow.md +299 -0
- zrb/task/llm/file_replacement.py +206 -0
- zrb/task/llm/file_tool_model.py +57 -0
- zrb/task/llm/history_processor.py +206 -0
- zrb/task/llm/history_summarization.py +2 -193
- zrb/task/llm/print_node.py +184 -64
- zrb/task/llm/prompt.py +175 -179
- zrb/task/llm/subagent_conversation_history.py +41 -0
- zrb/task/llm/tool_wrapper.py +226 -85
- zrb/task/llm/workflow.py +76 -0
- zrb/task/llm_task.py +109 -71
- zrb/task/make_task.py +2 -3
- zrb/task/rsync_task.py +25 -10
- zrb/task/scheduler.py +4 -4
- zrb/util/attr.py +54 -39
- zrb/util/cli/markdown.py +12 -0
- zrb/util/cli/text.py +30 -0
- zrb/util/file.py +12 -3
- zrb/util/git.py +2 -2
- zrb/util/{llm/prompt.py → markdown.py} +2 -3
- zrb/util/string/conversion.py +1 -1
- zrb/util/truncate.py +23 -0
- zrb/util/yaml.py +204 -0
- zrb/xcom/xcom.py +10 -0
- {zrb-1.15.3.dist-info → zrb-1.21.29.dist-info}/METADATA +38 -18
- {zrb-1.15.3.dist-info → zrb-1.21.29.dist-info}/RECORD +105 -79
- {zrb-1.15.3.dist-info → zrb-1.21.29.dist-info}/WHEEL +1 -1
- zrb/task/llm/default_workflow/coding.md +0 -24
- zrb/task/llm/default_workflow/copywriting.md +0 -17
- zrb/task/llm/default_workflow/researching.md +0 -18
- {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
|
|
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.
|
|
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
|
zrb/task/llm/print_node.py
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
-
# Reference: https://ai.pydantic.dev/agents/#streaming
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
event.delta,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
70
|
+
_format_content(
|
|
71
|
+
f"Result: tool_name={tool_name}", log_indent_level
|
|
72
|
+
)
|
|
48
73
|
)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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(
|
|
62
|
-
|
|
63
|
-
async
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
event.part.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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(
|
|
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:
|