zrb 1.5.4__py3-none-any.whl → 1.5.6__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/__init__.py +2 -0
- zrb/__main__.py +28 -2
- zrb/builtin/llm/history.py +73 -0
- zrb/builtin/llm/input.py +27 -0
- zrb/builtin/llm/llm_chat.py +4 -61
- zrb/builtin/llm/tool/api.py +39 -17
- zrb/builtin/llm/tool/cli.py +19 -5
- zrb/builtin/llm/tool/file.py +408 -405
- zrb/builtin/llm/tool/rag.py +18 -1
- zrb/builtin/llm/tool/web.py +31 -14
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/error.py +6 -8
- zrb/config.py +1 -0
- zrb/llm_config.py +81 -15
- zrb/task/llm/__init__.py +0 -0
- zrb/task/llm/agent_runner.py +53 -0
- zrb/task/llm/context_enricher.py +86 -0
- zrb/task/llm/default_context.py +44 -0
- zrb/task/llm/error.py +77 -0
- zrb/task/llm/history.py +92 -0
- zrb/task/llm/history_summarizer.py +71 -0
- zrb/task/llm/print_node.py +98 -0
- zrb/task/llm/tool_wrapper.py +88 -0
- zrb/task/llm_task.py +279 -246
- zrb/util/file.py +8 -2
- zrb/util/load.py +2 -0
- {zrb-1.5.4.dist-info → zrb-1.5.6.dist-info}/METADATA +1 -1
- {zrb-1.5.4.dist-info → zrb-1.5.6.dist-info}/RECORD +29 -18
- {zrb-1.5.4.dist-info → zrb-1.5.6.dist-info}/WHEEL +0 -0
- {zrb-1.5.4.dist-info → zrb-1.5.6.dist-info}/entry_points.txt +0 -0
zrb/__init__.py
CHANGED
@@ -43,6 +43,7 @@ from zrb.task.base_task import BaseTask
|
|
43
43
|
from zrb.task.base_trigger import BaseTrigger
|
44
44
|
from zrb.task.cmd_task import CmdTask
|
45
45
|
from zrb.task.http_check import HttpCheck
|
46
|
+
from zrb.task.llm.history import ConversationHistoryData
|
46
47
|
from zrb.task.llm_task import LLMTask
|
47
48
|
from zrb.task.make_task import make_task
|
48
49
|
from zrb.task.rsync_task import RsyncTask
|
@@ -91,6 +92,7 @@ assert BaseTrigger
|
|
91
92
|
assert RsyncTask
|
92
93
|
assert Task
|
93
94
|
assert LLMTask
|
95
|
+
assert ConversationHistoryData
|
94
96
|
assert Session
|
95
97
|
assert AnyContext
|
96
98
|
assert Context
|
zrb/__main__.py
CHANGED
@@ -1,26 +1,51 @@
|
|
1
|
+
import logging
|
1
2
|
import os
|
2
3
|
import sys
|
3
4
|
|
4
|
-
from zrb.config import INIT_MODULES, INIT_SCRIPTS
|
5
|
+
from zrb.config import INIT_MODULES, INIT_SCRIPTS, LOGGER, LOGGING_LEVEL
|
5
6
|
from zrb.runner.cli import cli
|
6
|
-
from zrb.util.cli.style import stylize_error, stylize_warning
|
7
|
+
from zrb.util.cli.style import stylize_error, stylize_faint, stylize_warning
|
7
8
|
from zrb.util.group import NodeNotFoundError
|
8
9
|
from zrb.util.load import load_file, load_module
|
9
10
|
|
10
11
|
|
12
|
+
# Custom Formatter for faint styling
|
13
|
+
class FaintFormatter(logging.Formatter):
|
14
|
+
|
15
|
+
def __init__(self, fmt=None, datefmt=None):
|
16
|
+
default_fmt = "%(asctime)s %(levelname)s: %(message)s"
|
17
|
+
default_datefmt = "%Y-%m-%d %H:%M:%S"
|
18
|
+
super().__init__(fmt=fmt or default_fmt, datefmt=datefmt or default_datefmt)
|
19
|
+
|
20
|
+
def format(self, record):
|
21
|
+
log_msg = super().format(record)
|
22
|
+
return stylize_faint(log_msg)
|
23
|
+
|
24
|
+
|
11
25
|
def serve_cli():
|
26
|
+
LOGGER.setLevel(LOGGING_LEVEL)
|
27
|
+
# Remove existing handlers to avoid duplicates/default formatting
|
28
|
+
for handler in LOGGER.handlers[:]:
|
29
|
+
LOGGER.removeHandler(handler)
|
30
|
+
handler = logging.StreamHandler()
|
31
|
+
handler.setFormatter(FaintFormatter())
|
32
|
+
LOGGER.addHandler(handler)
|
33
|
+
# --- End Logging Configuration ---
|
12
34
|
try:
|
13
35
|
# load init modules
|
14
36
|
for init_module in INIT_MODULES:
|
37
|
+
LOGGER.info(f"Loading {init_module}")
|
15
38
|
load_module(init_module)
|
16
39
|
zrb_init_path_list = _get_zrb_init_path_list()
|
17
40
|
# load init scripts
|
18
41
|
for init_script in INIT_SCRIPTS:
|
19
42
|
abs_init_script = os.path.abspath(os.path.expanduser(init_script))
|
20
43
|
if abs_init_script not in zrb_init_path_list:
|
44
|
+
LOGGER.info(f"Loading {abs_init_script}")
|
21
45
|
load_file(abs_init_script, -1)
|
22
46
|
# load zrb init
|
23
47
|
for zrb_init_path in zrb_init_path_list:
|
48
|
+
LOGGER.info(f"Loading {zrb_init_path}")
|
24
49
|
load_file(zrb_init_path)
|
25
50
|
# run the CLI
|
26
51
|
cli.run(sys.argv[1:])
|
@@ -45,6 +70,7 @@ def _get_zrb_init_path_list() -> list[str]:
|
|
45
70
|
zrb_init_path_list = []
|
46
71
|
for current_path in dir_path_list[::-1]:
|
47
72
|
zrb_init_path = os.path.join(current_path, "zrb_init.py")
|
73
|
+
LOGGER.info(f"Finding {zrb_init_path}")
|
48
74
|
if os.path.isfile(zrb_init_path):
|
49
75
|
zrb_init_path_list.append(zrb_init_path)
|
50
76
|
return zrb_init_path_list
|
@@ -0,0 +1,73 @@
|
|
1
|
+
import json
|
2
|
+
import os
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
from zrb.config import LLM_HISTORY_DIR
|
6
|
+
from zrb.context.any_shared_context import AnySharedContext
|
7
|
+
from zrb.task.llm.history import ConversationHistoryData
|
8
|
+
from zrb.util.file import read_file, write_file
|
9
|
+
|
10
|
+
|
11
|
+
def read_chat_conversation(ctx: AnySharedContext) -> dict[str, Any] | list | None:
|
12
|
+
"""Reads conversation history from the session file.
|
13
|
+
Returns the raw dictionary or list loaded from JSON, or None if not found/empty.
|
14
|
+
The LLMTask will handle parsing this into ConversationHistoryData.
|
15
|
+
"""
|
16
|
+
if ctx.input.start_new:
|
17
|
+
return None # Indicate no history to load
|
18
|
+
previous_session_name = ctx.input.previous_session
|
19
|
+
if not previous_session_name: # Check for empty string or None
|
20
|
+
last_session_file_path = os.path.join(LLM_HISTORY_DIR, "last-session")
|
21
|
+
if os.path.isfile(last_session_file_path):
|
22
|
+
previous_session_name = read_file(last_session_file_path).strip()
|
23
|
+
if not previous_session_name: # Handle empty last-session file
|
24
|
+
return None
|
25
|
+
else:
|
26
|
+
return None # No previous session specified and no last session found
|
27
|
+
conversation_file_path = os.path.join(
|
28
|
+
LLM_HISTORY_DIR, f"{previous_session_name}.json"
|
29
|
+
)
|
30
|
+
if not os.path.isfile(conversation_file_path):
|
31
|
+
ctx.log_warning(f"History file not found: {conversation_file_path}")
|
32
|
+
return None
|
33
|
+
try:
|
34
|
+
content = read_file(conversation_file_path)
|
35
|
+
if not content.strip():
|
36
|
+
ctx.log_warning(f"History file is empty: {conversation_file_path}")
|
37
|
+
return None
|
38
|
+
# Return the raw loaded data (dict or list)
|
39
|
+
return json.loads(content)
|
40
|
+
except json.JSONDecodeError:
|
41
|
+
ctx.log_warning(
|
42
|
+
f"Could not decode JSON from history file '{conversation_file_path}'. "
|
43
|
+
"Treating as empty history."
|
44
|
+
)
|
45
|
+
return None
|
46
|
+
except Exception as e:
|
47
|
+
ctx.log_warning(
|
48
|
+
f"Error reading history file '{conversation_file_path}': {e}. "
|
49
|
+
"Treating as empty history."
|
50
|
+
)
|
51
|
+
return None
|
52
|
+
|
53
|
+
|
54
|
+
def write_chat_conversation(
|
55
|
+
ctx: AnySharedContext, history_data: ConversationHistoryData
|
56
|
+
):
|
57
|
+
"""Writes the conversation history data (including context) to a session file."""
|
58
|
+
os.makedirs(LLM_HISTORY_DIR, exist_ok=True)
|
59
|
+
current_session_name = ctx.session.name
|
60
|
+
if not current_session_name:
|
61
|
+
ctx.log_warning("Cannot write history: Session name is empty.")
|
62
|
+
return
|
63
|
+
conversation_file_path = os.path.join(
|
64
|
+
LLM_HISTORY_DIR, f"{current_session_name}.json"
|
65
|
+
)
|
66
|
+
try:
|
67
|
+
# Use model_dump_json to serialize the Pydantic model
|
68
|
+
write_file(conversation_file_path, history_data.model_dump_json(indent=2))
|
69
|
+
# Update the last-session pointer
|
70
|
+
last_session_file_path = os.path.join(LLM_HISTORY_DIR, "last-session")
|
71
|
+
write_file(last_session_file_path, current_session_name)
|
72
|
+
except Exception as e:
|
73
|
+
ctx.log_error(f"Error writing history file '{conversation_file_path}': {e}")
|
zrb/builtin/llm/input.py
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
from zrb.context.any_shared_context import AnySharedContext
|
4
|
+
from zrb.input.str_input import StrInput
|
5
|
+
from zrb.util.file import read_file
|
6
|
+
from zrb.util.string.conversion import to_pascal_case
|
7
|
+
|
8
|
+
|
9
|
+
class PreviousSessionInput(StrInput):
|
10
|
+
|
11
|
+
def to_html(self, ctx: AnySharedContext) -> str:
|
12
|
+
name = self.name
|
13
|
+
description = self.description
|
14
|
+
default = self.get_default_str(ctx)
|
15
|
+
script = read_file(
|
16
|
+
file_path=os.path.join(os.path.dirname(__file__), "previous-session.js"),
|
17
|
+
replace_map={
|
18
|
+
"CURRENT_INPUT_NAME": name,
|
19
|
+
"CurrentPascalInputName": to_pascal_case(name),
|
20
|
+
},
|
21
|
+
)
|
22
|
+
return "\n".join(
|
23
|
+
[
|
24
|
+
f'<input name="{name}" placeholder="{description}" value="{default}" />',
|
25
|
+
f"<script>{script}</script>",
|
26
|
+
]
|
27
|
+
)
|
zrb/builtin/llm/llm_chat.py
CHANGED
@@ -1,8 +1,6 @@
|
|
1
|
-
import json
|
2
|
-
import os
|
3
|
-
from typing import Any
|
4
|
-
|
5
1
|
from zrb.builtin.group import llm_group
|
2
|
+
from zrb.builtin.llm.history import read_chat_conversation, write_chat_conversation
|
3
|
+
from zrb.builtin.llm.input import PreviousSessionInput
|
6
4
|
from zrb.builtin.llm.tool.api import get_current_location, get_current_weather
|
7
5
|
from zrb.builtin.llm.tool.cli import run_shell_command
|
8
6
|
from zrb.builtin.llm.tool.file import (
|
@@ -22,67 +20,12 @@ from zrb.config import (
|
|
22
20
|
LLM_ALLOW_ACCESS_INTERNET,
|
23
21
|
LLM_ALLOW_ACCESS_LOCAL_FILE,
|
24
22
|
LLM_ALLOW_ACCESS_SHELL,
|
25
|
-
LLM_HISTORY_DIR,
|
26
23
|
SERP_API_KEY,
|
27
24
|
)
|
28
|
-
from zrb.context.any_shared_context import AnySharedContext
|
29
25
|
from zrb.input.bool_input import BoolInput
|
30
26
|
from zrb.input.str_input import StrInput
|
31
27
|
from zrb.input.text_input import TextInput
|
32
28
|
from zrb.task.llm_task import LLMTask
|
33
|
-
from zrb.util.file import read_file, write_file
|
34
|
-
from zrb.util.string.conversion import to_pascal_case
|
35
|
-
|
36
|
-
|
37
|
-
class PreviousSessionInput(StrInput):
|
38
|
-
|
39
|
-
def to_html(self, ctx: AnySharedContext) -> str:
|
40
|
-
name = self.name
|
41
|
-
description = self.description
|
42
|
-
default = self.get_default_str(ctx)
|
43
|
-
script = read_file(
|
44
|
-
file_path=os.path.join(os.path.dirname(__file__), "previous-session.js"),
|
45
|
-
replace_map={
|
46
|
-
"CURRENT_INPUT_NAME": name,
|
47
|
-
"CurrentPascalInputName": to_pascal_case(name),
|
48
|
-
},
|
49
|
-
)
|
50
|
-
return "\n".join(
|
51
|
-
[
|
52
|
-
f'<input name="{name}" placeholder="{description}" value="{default}" />',
|
53
|
-
f"<script>{script}</script>",
|
54
|
-
]
|
55
|
-
)
|
56
|
-
|
57
|
-
|
58
|
-
def _read_chat_conversation(ctx: AnySharedContext) -> list[dict[str, Any]]:
|
59
|
-
if ctx.input.start_new:
|
60
|
-
return []
|
61
|
-
previous_session_name = ctx.input.previous_session
|
62
|
-
if previous_session_name == "" or previous_session_name is None:
|
63
|
-
last_session_file_path = os.path.join(LLM_HISTORY_DIR, "last-session")
|
64
|
-
if os.path.isfile(last_session_file_path):
|
65
|
-
previous_session_name = read_file(last_session_file_path).strip()
|
66
|
-
conversation_file_path = os.path.join(
|
67
|
-
LLM_HISTORY_DIR, f"{previous_session_name}.json"
|
68
|
-
)
|
69
|
-
if not os.path.isfile(conversation_file_path):
|
70
|
-
return []
|
71
|
-
return json.loads(read_file(conversation_file_path))
|
72
|
-
|
73
|
-
|
74
|
-
def _write_chat_conversation(
|
75
|
-
ctx: AnySharedContext, conversations: list[dict[str, Any]]
|
76
|
-
):
|
77
|
-
os.makedirs(LLM_HISTORY_DIR, exist_ok=True)
|
78
|
-
current_session_name = ctx.session.name
|
79
|
-
conversation_file_path = os.path.join(
|
80
|
-
LLM_HISTORY_DIR, f"{current_session_name}.json"
|
81
|
-
)
|
82
|
-
write_file(conversation_file_path, json.dumps(conversations, indent=2))
|
83
|
-
last_session_file_path = os.path.join(LLM_HISTORY_DIR, "last-session")
|
84
|
-
write_file(last_session_file_path, current_session_name)
|
85
|
-
|
86
29
|
|
87
30
|
llm_chat: LLMTask = llm_group.add_task(
|
88
31
|
LLMTask(
|
@@ -148,8 +91,8 @@ llm_chat: LLMTask = llm_group.add_task(
|
|
148
91
|
model_api_key=lambda ctx: (
|
149
92
|
None if ctx.input.api_key.strip() == "" else ctx.input.api_key
|
150
93
|
),
|
151
|
-
conversation_history_reader=
|
152
|
-
conversation_history_writer=
|
94
|
+
conversation_history_reader=read_chat_conversation,
|
95
|
+
conversation_history_writer=write_chat_conversation,
|
153
96
|
description="Chat with LLM",
|
154
97
|
system_prompt=lambda ctx: (
|
155
98
|
None if ctx.input.system_prompt.strip() == "" else ctx.input.system_prompt
|
zrb/builtin/llm/tool/api.py
CHANGED
@@ -1,14 +1,22 @@
|
|
1
1
|
import json
|
2
|
-
from typing import
|
2
|
+
from typing import Literal
|
3
3
|
|
4
4
|
|
5
|
-
def get_current_location() ->
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
def get_current_location() -> str:
|
6
|
+
"""Get current location (latitude, longitude) based on IP address.
|
7
|
+
Returns:
|
8
|
+
str: JSON string representing latitude and longitude.
|
9
|
+
Raises:
|
10
|
+
requests.RequestException: If the API request fails.
|
11
|
+
"""
|
9
12
|
import requests
|
10
13
|
|
11
|
-
|
14
|
+
try:
|
15
|
+
response = requests.get("http://ip-api.com/json?fields=lat,lon", timeout=5)
|
16
|
+
response.raise_for_status()
|
17
|
+
return json.dumps(response.json())
|
18
|
+
except requests.RequestException as e:
|
19
|
+
raise requests.RequestException(f"Failed to get location: {e}") from None
|
12
20
|
|
13
21
|
|
14
22
|
def get_current_weather(
|
@@ -16,16 +24,30 @@ def get_current_weather(
|
|
16
24
|
longitude: float,
|
17
25
|
temperature_unit: Literal["celsius", "fahrenheit"],
|
18
26
|
) -> str:
|
19
|
-
"""Get
|
27
|
+
"""Get current weather for a specific location.
|
28
|
+
Args:
|
29
|
+
latitude (float): Latitude coordinate.
|
30
|
+
longitude (float): Longitude coordinate.
|
31
|
+
temperature_unit (Literal["celsius", "fahrenheit"]): Temperature unit.
|
32
|
+
Returns:
|
33
|
+
str: JSON string with weather data.
|
34
|
+
Raises:
|
35
|
+
requests.RequestException: If the API request fails.
|
36
|
+
"""
|
20
37
|
import requests
|
21
38
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
39
|
+
try:
|
40
|
+
response = requests.get(
|
41
|
+
"https://api.open-meteo.com/v1/forecast",
|
42
|
+
params={
|
43
|
+
"latitude": latitude,
|
44
|
+
"longitude": longitude,
|
45
|
+
"temperature_unit": temperature_unit,
|
46
|
+
"current_weather": True,
|
47
|
+
},
|
48
|
+
timeout=5,
|
49
|
+
)
|
50
|
+
response.raise_for_status()
|
51
|
+
return json.dumps(response.json())
|
52
|
+
except requests.RequestException as e:
|
53
|
+
raise requests.RequestException(f"Failed to get weather data: {e}") from None
|
zrb/builtin/llm/tool/cli.py
CHANGED
@@ -2,8 +2,22 @@ import subprocess
|
|
2
2
|
|
3
3
|
|
4
4
|
def run_shell_command(command: str) -> str:
|
5
|
-
"""
|
6
|
-
|
7
|
-
command
|
8
|
-
|
9
|
-
|
5
|
+
"""Execute a shell command and return its combined stdout and stderr.
|
6
|
+
Args:
|
7
|
+
command (str): Shell command to execute on the user's system.
|
8
|
+
Returns:
|
9
|
+
str: The command's output (stdout and stderr combined).
|
10
|
+
Raises:
|
11
|
+
subprocess.CalledProcessError: If the command returns a non-zero exit code.
|
12
|
+
subprocess.SubprocessError: If there's an issue with subprocess execution.
|
13
|
+
"""
|
14
|
+
try:
|
15
|
+
output = subprocess.check_output(
|
16
|
+
command, shell=True, stderr=subprocess.STDOUT, text=True
|
17
|
+
)
|
18
|
+
return output
|
19
|
+
except subprocess.CalledProcessError as e:
|
20
|
+
# Include the error output in the exception message
|
21
|
+
raise subprocess.CalledProcessError(
|
22
|
+
e.returncode, e.cmd, e.output, e.stderr
|
23
|
+
) from None
|