zrb 1.15.3__py3-none-any.whl ā 2.0.0a4__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 +118 -133
- zrb/attr/type.py +10 -7
- zrb/builtin/__init__.py +55 -1
- zrb/builtin/git.py +12 -1
- zrb/builtin/group.py +31 -15
- zrb/builtin/llm/chat.py +147 -0
- 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/callback/callback.py +8 -1
- zrb/cmd/cmd_result.py +2 -1
- zrb/config/config.py +555 -169
- zrb/config/helper.py +84 -0
- zrb/config/web_auth_config.py +50 -35
- zrb/context/any_shared_context.py +20 -3
- zrb/context/context.py +39 -5
- zrb/context/print_fn.py +13 -0
- zrb/context/shared_context.py +17 -8
- 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 +41 -1
- zrb/input/text_input.py +7 -24
- zrb/llm/agent/__init__.py +9 -0
- zrb/llm/agent/agent.py +215 -0
- zrb/llm/agent/summarizer.py +20 -0
- zrb/llm/app/__init__.py +10 -0
- zrb/llm/app/completion.py +281 -0
- zrb/llm/app/confirmation/allow_tool.py +66 -0
- zrb/llm/app/confirmation/handler.py +178 -0
- zrb/llm/app/confirmation/replace_confirmation.py +77 -0
- zrb/llm/app/keybinding.py +34 -0
- zrb/llm/app/layout.py +117 -0
- zrb/llm/app/lexer.py +155 -0
- zrb/llm/app/redirection.py +28 -0
- zrb/llm/app/style.py +16 -0
- zrb/llm/app/ui.py +733 -0
- zrb/llm/config/__init__.py +4 -0
- zrb/llm/config/config.py +122 -0
- zrb/llm/config/limiter.py +247 -0
- zrb/llm/history_manager/__init__.py +4 -0
- zrb/llm/history_manager/any_history_manager.py +23 -0
- zrb/llm/history_manager/file_history_manager.py +91 -0
- zrb/llm/history_processor/summarizer.py +108 -0
- zrb/llm/note/__init__.py +3 -0
- zrb/llm/note/manager.py +122 -0
- zrb/llm/prompt/__init__.py +29 -0
- zrb/llm/prompt/claude_compatibility.py +92 -0
- zrb/llm/prompt/compose.py +55 -0
- zrb/llm/prompt/default.py +51 -0
- zrb/llm/prompt/markdown/file_extractor.md +112 -0
- zrb/llm/prompt/markdown/mandate.md +23 -0
- zrb/llm/prompt/markdown/persona.md +3 -0
- zrb/llm/prompt/markdown/repo_extractor.md +112 -0
- zrb/llm/prompt/markdown/repo_summarizer.md +29 -0
- zrb/llm/prompt/markdown/summarizer.md +21 -0
- zrb/llm/prompt/note.py +41 -0
- zrb/llm/prompt/system_context.py +46 -0
- zrb/llm/prompt/zrb.py +41 -0
- zrb/llm/skill/__init__.py +3 -0
- zrb/llm/skill/manager.py +86 -0
- zrb/llm/task/__init__.py +4 -0
- zrb/llm/task/llm_chat_task.py +316 -0
- zrb/llm/task/llm_task.py +245 -0
- zrb/llm/tool/__init__.py +39 -0
- zrb/llm/tool/bash.py +75 -0
- zrb/llm/tool/code.py +266 -0
- zrb/llm/tool/file.py +419 -0
- zrb/llm/tool/note.py +70 -0
- zrb/{builtin/llm ā llm}/tool/rag.py +33 -37
- zrb/llm/tool/search/brave.py +53 -0
- zrb/llm/tool/search/searxng.py +47 -0
- zrb/llm/tool/search/serpapi.py +47 -0
- zrb/llm/tool/skill.py +19 -0
- zrb/llm/tool/sub_agent.py +70 -0
- zrb/llm/tool/web.py +97 -0
- zrb/llm/tool/zrb_task.py +66 -0
- zrb/llm/util/attachment.py +101 -0
- zrb/llm/util/prompt.py +104 -0
- zrb/llm/util/stream_response.py +178 -0
- 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 -9
- zrb/session/session.py +38 -17
- zrb/task/any_task.py +24 -3
- zrb/task/base/context.py +42 -22
- zrb/task/base/execution.py +67 -55
- zrb/task/base/lifecycle.py +14 -7
- zrb/task/base/monitoring.py +12 -7
- zrb/task/base_task.py +113 -50
- zrb/task/base_trigger.py +16 -6
- zrb/task/cmd_task.py +6 -0
- zrb/task/http_check.py +11 -5
- zrb/task/make_task.py +5 -3
- zrb/task/rsync_task.py +30 -10
- zrb/task/scaffolder.py +7 -4
- zrb/task/scheduler.py +7 -4
- zrb/task/tcp_check.py +6 -4
- zrb/util/ascii_art/art/bee.txt +17 -0
- zrb/util/ascii_art/art/cat.txt +9 -0
- zrb/util/ascii_art/art/ghost.txt +16 -0
- zrb/util/ascii_art/art/panda.txt +17 -0
- zrb/util/ascii_art/art/rose.txt +14 -0
- zrb/util/ascii_art/art/unicorn.txt +15 -0
- zrb/util/ascii_art/banner.py +92 -0
- zrb/util/attr.py +54 -39
- zrb/util/cli/markdown.py +32 -0
- zrb/util/cli/text.py +30 -0
- zrb/util/cmd/command.py +33 -10
- zrb/util/file.py +61 -33
- zrb/util/git.py +2 -2
- zrb/util/{llm/prompt.py ā markdown.py} +2 -3
- zrb/util/match.py +78 -0
- zrb/util/run.py +3 -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-2.0.0a4.dist-info}/METADATA +41 -27
- {zrb-1.15.3.dist-info ā zrb-2.0.0a4.dist-info}/RECORD +129 -131
- {zrb-1.15.3.dist-info ā zrb-2.0.0a4.dist-info}/WHEEL +1 -1
- zrb/attr/__init__.py +0 -0
- zrb/builtin/llm/chat_session.py +0 -311
- zrb/builtin/llm/history.py +0 -71
- zrb/builtin/llm/input.py +0 -27
- zrb/builtin/llm/llm_ask.py +0 -187
- zrb/builtin/llm/previous-session.js +0 -21
- zrb/builtin/llm/tool/__init__.py +0 -0
- zrb/builtin/llm/tool/api.py +0 -71
- zrb/builtin/llm/tool/cli.py +0 -38
- zrb/builtin/llm/tool/code.py +0 -254
- zrb/builtin/llm/tool/file.py +0 -626
- zrb/builtin/llm/tool/sub_agent.py +0 -137
- zrb/builtin/llm/tool/web.py +0 -195
- zrb/builtin/project/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/service/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/__init__.py +0 -0
- zrb/builtin/project/create/__init__.py +0 -0
- zrb/builtin/shell/__init__.py +0 -0
- zrb/builtin/shell/autocomplete/__init__.py +0 -0
- zrb/callback/__init__.py +0 -0
- zrb/cmd/__init__.py +0 -0
- zrb/config/default_prompt/file_extractor_system_prompt.md +0 -12
- zrb/config/default_prompt/interactive_system_prompt.md +0 -35
- zrb/config/default_prompt/persona.md +0 -1
- zrb/config/default_prompt/repo_extractor_system_prompt.md +0 -112
- zrb/config/default_prompt/repo_summarizer_system_prompt.md +0 -10
- zrb/config/default_prompt/summarization_prompt.md +0 -16
- zrb/config/default_prompt/system_prompt.md +0 -32
- zrb/config/llm_config.py +0 -243
- zrb/config/llm_context/config.py +0 -129
- zrb/config/llm_context/config_parser.py +0 -46
- zrb/config/llm_rate_limitter.py +0 -137
- zrb/content_transformer/__init__.py +0 -0
- zrb/context/__init__.py +0 -0
- zrb/dot_dict/__init__.py +0 -0
- zrb/env/__init__.py +0 -0
- zrb/group/__init__.py +0 -0
- zrb/input/__init__.py +0 -0
- zrb/runner/__init__.py +0 -0
- zrb/runner/web_route/__init__.py +0 -0
- zrb/runner/web_route/home_page/__init__.py +0 -0
- zrb/session/__init__.py +0 -0
- zrb/session_state_log/__init__.py +0 -0
- zrb/session_state_logger/__init__.py +0 -0
- zrb/task/__init__.py +0 -0
- zrb/task/base/__init__.py +0 -0
- zrb/task/llm/__init__.py +0 -0
- zrb/task/llm/agent.py +0 -243
- zrb/task/llm/config.py +0 -103
- zrb/task/llm/conversation_history.py +0 -128
- zrb/task/llm/conversation_history_model.py +0 -242
- 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/task/llm/error.py +0 -95
- zrb/task/llm/history_summarization.py +0 -216
- zrb/task/llm/print_node.py +0 -101
- zrb/task/llm/prompt.py +0 -325
- zrb/task/llm/tool_wrapper.py +0 -220
- zrb/task/llm/typing.py +0 -3
- zrb/task/llm_task.py +0 -341
- zrb/task_status/__init__.py +0 -0
- zrb/util/__init__.py +0 -0
- zrb/util/cli/__init__.py +0 -0
- zrb/util/cmd/__init__.py +0 -0
- zrb/util/codemod/__init__.py +0 -0
- zrb/util/string/__init__.py +0 -0
- zrb/xcom/__init__.py +0 -0
- {zrb-1.15.3.dist-info ā zrb-2.0.0a4.dist-info}/entry_points.txt +0 -0
zrb/llm/app/ui.py
ADDED
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import TYPE_CHECKING, Any, TextIO
|
|
8
|
+
|
|
9
|
+
from prompt_toolkit import Application
|
|
10
|
+
from prompt_toolkit.application import get_app
|
|
11
|
+
from prompt_toolkit.document import Document
|
|
12
|
+
from prompt_toolkit.formatted_text import AnyFormattedText
|
|
13
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
14
|
+
from prompt_toolkit.layout import Layout
|
|
15
|
+
from prompt_toolkit.lexers import Lexer
|
|
16
|
+
from prompt_toolkit.patch_stdout import patch_stdout
|
|
17
|
+
from prompt_toolkit.styles import Style
|
|
18
|
+
|
|
19
|
+
from zrb.context.shared_context import SharedContext
|
|
20
|
+
from zrb.llm.app.confirmation.handler import (
|
|
21
|
+
ConfirmationHandler,
|
|
22
|
+
ConfirmationMiddleware,
|
|
23
|
+
last_confirmation,
|
|
24
|
+
)
|
|
25
|
+
from zrb.llm.app.keybinding import create_output_keybindings
|
|
26
|
+
from zrb.llm.app.layout import create_input_field, create_layout, create_output_field
|
|
27
|
+
from zrb.llm.app.redirection import StreamToUI
|
|
28
|
+
from zrb.llm.app.style import create_style
|
|
29
|
+
from zrb.llm.history_manager.any_history_manager import AnyHistoryManager
|
|
30
|
+
from zrb.llm.task.llm_task import LLMTask
|
|
31
|
+
from zrb.session.any_session import AnySession
|
|
32
|
+
from zrb.session.session import Session
|
|
33
|
+
from zrb.task.any_task import AnyTask
|
|
34
|
+
from zrb.util.ascii_art.banner import create_banner
|
|
35
|
+
from zrb.util.cli.markdown import render_markdown
|
|
36
|
+
from zrb.util.cli.style import stylize_faint
|
|
37
|
+
from zrb.util.string.name import get_random_name
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from pydantic_ai import UserContent
|
|
41
|
+
from pydantic_ai.models import Model
|
|
42
|
+
from rich.theme import Theme
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class UI:
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
greeting: str,
|
|
49
|
+
assistant_name: str,
|
|
50
|
+
ascii_art: str,
|
|
51
|
+
jargon: str,
|
|
52
|
+
output_lexer: Lexer,
|
|
53
|
+
llm_task: LLMTask,
|
|
54
|
+
history_manager: AnyHistoryManager,
|
|
55
|
+
initial_message: Any = "",
|
|
56
|
+
initial_attachments: "list[UserContent]" = [],
|
|
57
|
+
conversation_session_name: str = "",
|
|
58
|
+
yolo: bool = False,
|
|
59
|
+
triggers: list[Callable[[], Any]] = [],
|
|
60
|
+
confirmation_middlewares: list[ConfirmationMiddleware] = [],
|
|
61
|
+
markdown_theme: "Theme | None" = None,
|
|
62
|
+
summarize_commands: list[str] = [],
|
|
63
|
+
attach_commands: list[str] = [],
|
|
64
|
+
exit_commands: list[str] = [],
|
|
65
|
+
info_commands: list[str] = [],
|
|
66
|
+
save_commands: list[str] = [],
|
|
67
|
+
load_commands: list[str] = [],
|
|
68
|
+
redirect_output_commands: list[str] = [],
|
|
69
|
+
yolo_toggle_commands: list[str] = [],
|
|
70
|
+
exec_commands: list[str] = [],
|
|
71
|
+
model: "Model | str | None" = None,
|
|
72
|
+
):
|
|
73
|
+
self._is_thinking = False
|
|
74
|
+
self._running_llm_task: asyncio.Task | None = None
|
|
75
|
+
self._llm_task = llm_task
|
|
76
|
+
self._history_manager = history_manager
|
|
77
|
+
self._assistant_name = assistant_name
|
|
78
|
+
self._ascii_art = ascii_art
|
|
79
|
+
self._jargon = jargon
|
|
80
|
+
self._initial_message = initial_message
|
|
81
|
+
self._conversation_session_name = conversation_session_name
|
|
82
|
+
if not self._conversation_session_name:
|
|
83
|
+
self._conversation_session_name = get_random_name()
|
|
84
|
+
self._yolo = yolo
|
|
85
|
+
self._model = model
|
|
86
|
+
self._triggers = triggers
|
|
87
|
+
self._markdown_theme = markdown_theme
|
|
88
|
+
self._summarize_commands = summarize_commands
|
|
89
|
+
self._attach_commands = attach_commands
|
|
90
|
+
self._exit_commands = exit_commands
|
|
91
|
+
self._info_commands = info_commands
|
|
92
|
+
self._save_commands = save_commands
|
|
93
|
+
self._load_commands = load_commands
|
|
94
|
+
self._redirect_output_commands = redirect_output_commands
|
|
95
|
+
self._yolo_toggle_commands = yolo_toggle_commands
|
|
96
|
+
self._exec_commands = exec_commands
|
|
97
|
+
self._trigger_tasks: list[asyncio.Task] = []
|
|
98
|
+
self._last_result_data: str | None = None
|
|
99
|
+
# Attachments
|
|
100
|
+
self._pending_attachments: "list[UserContent]" = list(initial_attachments)
|
|
101
|
+
# Confirmation Handler
|
|
102
|
+
self._confirmation_handler = ConfirmationHandler(
|
|
103
|
+
middlewares=confirmation_middlewares + [last_confirmation]
|
|
104
|
+
)
|
|
105
|
+
# Confirmation state (Used by ask_user and keybindings)
|
|
106
|
+
self._waiting_for_confirmation = False
|
|
107
|
+
self._confirmation_future: asyncio.Future[str] | None = None
|
|
108
|
+
# UI Styles
|
|
109
|
+
self._style = create_style()
|
|
110
|
+
# Input Area
|
|
111
|
+
self._input_field = create_input_field(
|
|
112
|
+
history_manager=self._history_manager,
|
|
113
|
+
attach_commands=self._attach_commands,
|
|
114
|
+
exit_commands=self._exit_commands,
|
|
115
|
+
info_commands=self._info_commands,
|
|
116
|
+
save_commands=self._save_commands,
|
|
117
|
+
load_commands=self._load_commands,
|
|
118
|
+
redirect_output_commands=self._redirect_output_commands,
|
|
119
|
+
summarize_commands=self._summarize_commands,
|
|
120
|
+
exec_commands=self._exec_commands,
|
|
121
|
+
)
|
|
122
|
+
# Output Area (Read-only chat history)
|
|
123
|
+
help_text = self._get_help_text()
|
|
124
|
+
full_greeting = create_banner(self._ascii_art, f"{greeting}\n{help_text}")
|
|
125
|
+
self._output_field = create_output_field(full_greeting, output_lexer)
|
|
126
|
+
self._output_field.control.key_bindings = create_output_keybindings(
|
|
127
|
+
self._input_field
|
|
128
|
+
)
|
|
129
|
+
self._layout = create_layout(
|
|
130
|
+
title=self._assistant_name,
|
|
131
|
+
jargon=self._jargon,
|
|
132
|
+
input_field=self._input_field,
|
|
133
|
+
output_field=self._output_field,
|
|
134
|
+
info_bar_text=self._get_info_bar_text,
|
|
135
|
+
status_bar_text=self._get_status_bar_text,
|
|
136
|
+
)
|
|
137
|
+
# Key Bindings
|
|
138
|
+
self._app_kb = KeyBindings()
|
|
139
|
+
self._setup_app_keybindings(
|
|
140
|
+
app_keybindings=self._app_kb, llm_task=self._llm_task
|
|
141
|
+
)
|
|
142
|
+
# Application
|
|
143
|
+
self._application = self._create_application(
|
|
144
|
+
layout=self._layout, keybindings=self._app_kb, style=self._style
|
|
145
|
+
)
|
|
146
|
+
# Send message if first_message is provided. Make sure only run at most once
|
|
147
|
+
if self._initial_message:
|
|
148
|
+
self._application.after_render.add_handler(self._on_first_render)
|
|
149
|
+
|
|
150
|
+
async def run_async(self):
|
|
151
|
+
"""Run the application and manage triggers."""
|
|
152
|
+
import logging
|
|
153
|
+
|
|
154
|
+
# Start triggers
|
|
155
|
+
for trigger_fn in self._triggers:
|
|
156
|
+
trigger_task = self._application.create_background_task(
|
|
157
|
+
self._trigger_loop(trigger_fn)
|
|
158
|
+
)
|
|
159
|
+
self._trigger_tasks.append(trigger_task)
|
|
160
|
+
|
|
161
|
+
# Setup logging redirection to UI
|
|
162
|
+
root_logger = logging.getLogger()
|
|
163
|
+
original_handlers = root_logger.handlers[:]
|
|
164
|
+
|
|
165
|
+
ui_stream = StreamToUI(self.append_to_output)
|
|
166
|
+
ui_handler = logging.StreamHandler(ui_stream)
|
|
167
|
+
formatter = logging.Formatter("[%(levelname)s] %(name)s: %(message)s")
|
|
168
|
+
ui_handler.setFormatter(formatter)
|
|
169
|
+
|
|
170
|
+
root_logger.handlers = [ui_handler]
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
with patch_stdout():
|
|
174
|
+
return await self._application.run_async()
|
|
175
|
+
finally:
|
|
176
|
+
# Restore handlers
|
|
177
|
+
root_logger.handlers = original_handlers
|
|
178
|
+
|
|
179
|
+
# Stop triggers
|
|
180
|
+
for trigger_task in self._trigger_tasks:
|
|
181
|
+
trigger_task.cancel()
|
|
182
|
+
self._trigger_tasks.clear()
|
|
183
|
+
|
|
184
|
+
async def _trigger_loop(self, trigger_fn: Callable[[], Any]):
|
|
185
|
+
"""Handle external triggers and submit user message when trigger activated"""
|
|
186
|
+
while True:
|
|
187
|
+
try:
|
|
188
|
+
if asyncio.iscoroutinefunction(trigger_fn):
|
|
189
|
+
result = await trigger_fn()
|
|
190
|
+
else:
|
|
191
|
+
result = await asyncio.to_thread(trigger_fn)
|
|
192
|
+
|
|
193
|
+
if result:
|
|
194
|
+
self._submit_user_message(self._llm_task, str(result))
|
|
195
|
+
|
|
196
|
+
except asyncio.CancelledError:
|
|
197
|
+
break
|
|
198
|
+
except Exception:
|
|
199
|
+
# Keep running on error, maybe log it
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
def _on_first_render(self, app: Application):
|
|
203
|
+
"""Handle initial message (the message sent when creating the UI)"""
|
|
204
|
+
self._application.after_render.remove_handler(self._on_first_render)
|
|
205
|
+
self._submit_user_message(self._llm_task, self._initial_message)
|
|
206
|
+
|
|
207
|
+
async def ask_user(self, prompt: str) -> str:
|
|
208
|
+
"""Prompts the user for input via the main input field, blocking until provided."""
|
|
209
|
+
self._waiting_for_confirmation = True
|
|
210
|
+
self._confirmation_future = asyncio.Future()
|
|
211
|
+
|
|
212
|
+
if prompt:
|
|
213
|
+
self.append_to_output(prompt, end="")
|
|
214
|
+
|
|
215
|
+
get_app().invalidate()
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
return await self._confirmation_future
|
|
219
|
+
finally:
|
|
220
|
+
self._waiting_for_confirmation = False
|
|
221
|
+
self._confirmation_future = None
|
|
222
|
+
|
|
223
|
+
async def _confirm_tool_execution(self, call: Any) -> Any:
|
|
224
|
+
return await self._confirmation_handler.handle(self, call)
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def triggers(self) -> list[Callable[[], Any]]:
|
|
228
|
+
return self._triggers
|
|
229
|
+
|
|
230
|
+
@triggers.setter
|
|
231
|
+
def triggers(self, value: list[Callable[[], Any]]):
|
|
232
|
+
self._triggers = value
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def application(self) -> Application:
|
|
236
|
+
return self._application
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def last_output(self) -> str:
|
|
240
|
+
if self._last_result_data is None:
|
|
241
|
+
return ""
|
|
242
|
+
return self._last_result_data
|
|
243
|
+
|
|
244
|
+
def _create_application(
|
|
245
|
+
self,
|
|
246
|
+
layout: Layout,
|
|
247
|
+
keybindings: KeyBindings,
|
|
248
|
+
style: Style,
|
|
249
|
+
) -> Application:
|
|
250
|
+
return Application(
|
|
251
|
+
layout=layout,
|
|
252
|
+
key_bindings=keybindings,
|
|
253
|
+
style=style,
|
|
254
|
+
full_screen=True,
|
|
255
|
+
mouse_support=True,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def _get_help_text(self) -> str:
|
|
259
|
+
help_lines = ["\nAvailable Commands:"]
|
|
260
|
+
|
|
261
|
+
def add_cmd_help(commands: list[str], description: str):
|
|
262
|
+
if commands and len(commands) > 0:
|
|
263
|
+
cmd = commands[0]
|
|
264
|
+
description = description.replace("{cmd}", cmd)
|
|
265
|
+
help_lines.append(f" {cmd:<10} : {description}")
|
|
266
|
+
|
|
267
|
+
add_cmd_help(self._exit_commands, "Exit the application")
|
|
268
|
+
add_cmd_help(self._info_commands, "Show this help message")
|
|
269
|
+
add_cmd_help(self._attach_commands, "Attach file (usage: {cmd} <path>)")
|
|
270
|
+
add_cmd_help(self._save_commands, "Save conversation (usage: {cmd} <name>)")
|
|
271
|
+
add_cmd_help(self._load_commands, "Load conversation (usage: {cmd} <name>)")
|
|
272
|
+
add_cmd_help(
|
|
273
|
+
self._redirect_output_commands,
|
|
274
|
+
"Save last output to file (usage: {cmd} <file>)",
|
|
275
|
+
)
|
|
276
|
+
add_cmd_help(self._summarize_commands, "Summarize conversation history")
|
|
277
|
+
add_cmd_help(self._yolo_toggle_commands, "Toggle YOLO mode")
|
|
278
|
+
add_cmd_help(
|
|
279
|
+
self._exec_commands, "Execute shell command (usage: {cmd} <command>)"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
return "\n".join(help_lines) + "\n"
|
|
283
|
+
|
|
284
|
+
def _get_info_bar_text(self) -> AnyFormattedText:
|
|
285
|
+
from prompt_toolkit.formatted_text import HTML
|
|
286
|
+
|
|
287
|
+
model_name = "Unknown"
|
|
288
|
+
if self._model:
|
|
289
|
+
if isinstance(self._model, str):
|
|
290
|
+
model_name = self._model
|
|
291
|
+
elif hasattr(self._model, "model_name"):
|
|
292
|
+
model_name = getattr(self._model, "model_name")
|
|
293
|
+
else:
|
|
294
|
+
model_name = str(self._model)
|
|
295
|
+
yolo_text = (
|
|
296
|
+
"<style color='ansired'><b>ON</b></style>"
|
|
297
|
+
if self._yolo
|
|
298
|
+
else "<style color='ansigreen'>OFF</style>"
|
|
299
|
+
)
|
|
300
|
+
return HTML(
|
|
301
|
+
f" š¤ <b>Model:</b> {model_name} "
|
|
302
|
+
f"| š£ļø <b>Session:</b> {self._conversation_session_name} "
|
|
303
|
+
f"| š¤ <b>YOLO:</b> {yolo_text} "
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def _get_status_bar_text(self) -> AnyFormattedText:
|
|
307
|
+
if self._is_thinking:
|
|
308
|
+
return [("class:thinking", f" {self._assistant_name} is thinking... ")]
|
|
309
|
+
return [("class:status", " Ready ")]
|
|
310
|
+
|
|
311
|
+
def _setup_app_keybindings(self, app_keybindings: KeyBindings, llm_task: AnyTask):
|
|
312
|
+
@app_keybindings.add("c-c")
|
|
313
|
+
def _(event):
|
|
314
|
+
# If text is selected, copy it instead of exiting
|
|
315
|
+
buffer = event.app.current_buffer
|
|
316
|
+
if buffer.selection_state:
|
|
317
|
+
data = buffer.copy_selection()
|
|
318
|
+
event.app.clipboard.set_data(data)
|
|
319
|
+
buffer.exit_selection()
|
|
320
|
+
return
|
|
321
|
+
# If buffer is not empty, clear it
|
|
322
|
+
if buffer.text != "":
|
|
323
|
+
buffer.reset()
|
|
324
|
+
return
|
|
325
|
+
event.app.exit()
|
|
326
|
+
|
|
327
|
+
@app_keybindings.add("escape")
|
|
328
|
+
def _(event):
|
|
329
|
+
if self._running_llm_task and not self._running_llm_task.done():
|
|
330
|
+
self._running_llm_task.cancel()
|
|
331
|
+
self.append_to_output("\n<Esc> Canceled")
|
|
332
|
+
|
|
333
|
+
@app_keybindings.add("enter")
|
|
334
|
+
def _(event):
|
|
335
|
+
# Handle confirmation and multiline
|
|
336
|
+
if self._handle_confirmation(event):
|
|
337
|
+
return
|
|
338
|
+
if self._handle_multiline(event):
|
|
339
|
+
return
|
|
340
|
+
|
|
341
|
+
# Handle empty inputs
|
|
342
|
+
buff = event.current_buffer
|
|
343
|
+
text = buff.text
|
|
344
|
+
if not text.strip():
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
# Handle other commands
|
|
348
|
+
if self._handle_exit_command(event):
|
|
349
|
+
return
|
|
350
|
+
if self._handle_info_command(event):
|
|
351
|
+
return
|
|
352
|
+
if self._handle_save_command(event):
|
|
353
|
+
return
|
|
354
|
+
if self._handle_load_command(event):
|
|
355
|
+
return
|
|
356
|
+
if self._handle_redirect_command(event):
|
|
357
|
+
return
|
|
358
|
+
if self._handle_attach_command(event):
|
|
359
|
+
return
|
|
360
|
+
if self._handle_toggle_yolo(event):
|
|
361
|
+
return
|
|
362
|
+
if self._handle_exec_command(event):
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
# If we are thinking, ignore input
|
|
366
|
+
if self._is_thinking:
|
|
367
|
+
return
|
|
368
|
+
self._submit_user_message(llm_task, text)
|
|
369
|
+
buff.reset()
|
|
370
|
+
|
|
371
|
+
@app_keybindings.add("c-j") # Ctrl+J
|
|
372
|
+
@app_keybindings.add("c-space") # Ctrl+Space (Fallback)
|
|
373
|
+
def _(event):
|
|
374
|
+
event.current_buffer.insert_text("\n")
|
|
375
|
+
|
|
376
|
+
def _handle_exec_command(self, event) -> bool:
|
|
377
|
+
buff = event.current_buffer
|
|
378
|
+
text = buff.text
|
|
379
|
+
for cmd in self._exec_commands:
|
|
380
|
+
prefix = f"{cmd} "
|
|
381
|
+
if text.strip().lower().startswith(prefix):
|
|
382
|
+
if self._is_thinking:
|
|
383
|
+
return False
|
|
384
|
+
|
|
385
|
+
shell_cmd = text.strip()[len(prefix) :].strip()
|
|
386
|
+
if not shell_cmd:
|
|
387
|
+
return True
|
|
388
|
+
|
|
389
|
+
buff.reset()
|
|
390
|
+
# Run in background
|
|
391
|
+
self._running_llm_task = asyncio.create_task(
|
|
392
|
+
self._run_shell_command(shell_cmd)
|
|
393
|
+
)
|
|
394
|
+
return True
|
|
395
|
+
return False
|
|
396
|
+
|
|
397
|
+
async def _run_shell_command(self, cmd: str):
|
|
398
|
+
self._is_thinking = True
|
|
399
|
+
get_app().invalidate()
|
|
400
|
+
timestamp = datetime.now().strftime("%H:%M")
|
|
401
|
+
|
|
402
|
+
try:
|
|
403
|
+
self.append_to_output(f"\nš» {timestamp} >>\n$ {cmd}\n")
|
|
404
|
+
self.append_to_output(f"\n š¢ Executing...\n")
|
|
405
|
+
|
|
406
|
+
# Create subprocess
|
|
407
|
+
process = await asyncio.create_subprocess_shell(
|
|
408
|
+
cmd,
|
|
409
|
+
stdout=asyncio.subprocess.PIPE,
|
|
410
|
+
stderr=asyncio.subprocess.PIPE,
|
|
411
|
+
)
|
|
412
|
+
is_first_output = True
|
|
413
|
+
|
|
414
|
+
# Read output streams
|
|
415
|
+
async def read_stream(stream, is_stderr=False):
|
|
416
|
+
while True:
|
|
417
|
+
nonlocal is_first_output
|
|
418
|
+
line = await stream.readline()
|
|
419
|
+
if not line:
|
|
420
|
+
break
|
|
421
|
+
decoded_line = line.decode("utf-8", errors="replace")
|
|
422
|
+
decoded_line = decoded_line.replace("\n", "\n ").replace(
|
|
423
|
+
"\r", "\r "
|
|
424
|
+
)
|
|
425
|
+
if is_first_output:
|
|
426
|
+
decoded_line = f" {decoded_line}"
|
|
427
|
+
is_first_output = False
|
|
428
|
+
# Could use a different color for stderr if desired
|
|
429
|
+
self.append_to_output(decoded_line, end="")
|
|
430
|
+
|
|
431
|
+
await asyncio.gather(
|
|
432
|
+
read_stream(process.stdout), read_stream(process.stderr, is_stderr=True)
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
return_code = await process.wait()
|
|
436
|
+
|
|
437
|
+
if return_code == 0:
|
|
438
|
+
self.append_to_output(f"\n ā
Command finished successfully.\n")
|
|
439
|
+
else:
|
|
440
|
+
self.append_to_output(
|
|
441
|
+
f"\n ā Command failed with exit code {return_code}.\n"
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
except asyncio.CancelledError:
|
|
445
|
+
self.append_to_output("\n[Cancelled]\n")
|
|
446
|
+
except Exception as e:
|
|
447
|
+
self.append_to_output(f"\n[Error: {e}]\n")
|
|
448
|
+
finally:
|
|
449
|
+
self._is_thinking = False
|
|
450
|
+
self._running_llm_task = None
|
|
451
|
+
get_app().invalidate()
|
|
452
|
+
|
|
453
|
+
def _handle_toggle_yolo(self, event) -> bool:
|
|
454
|
+
buff = event.current_buffer
|
|
455
|
+
text = buff.text
|
|
456
|
+
if text.strip().lower() in self._yolo_toggle_commands:
|
|
457
|
+
if self._is_thinking:
|
|
458
|
+
return False
|
|
459
|
+
self._yolo = not self._yolo
|
|
460
|
+
buff.reset()
|
|
461
|
+
return True
|
|
462
|
+
return False
|
|
463
|
+
|
|
464
|
+
def _handle_multiline(self, event) -> bool:
|
|
465
|
+
buff = event.current_buffer
|
|
466
|
+
text = buff.text
|
|
467
|
+
# Check for multiline indicator (trailing backslash)
|
|
468
|
+
if text.strip().endswith("\\"):
|
|
469
|
+
# If cursor is at the end, remove backslash and insert newline
|
|
470
|
+
if buff.cursor_position == len(text):
|
|
471
|
+
# Remove the backslash (assuming it's the last char)
|
|
472
|
+
# We need to be careful with whitespace after backslash if we used strip()
|
|
473
|
+
# Let's just check the character before cursor
|
|
474
|
+
if text.endswith("\\"):
|
|
475
|
+
buff.delete_before_cursor(count=1)
|
|
476
|
+
buff.insert_text("\n")
|
|
477
|
+
return True
|
|
478
|
+
return False
|
|
479
|
+
|
|
480
|
+
def _handle_confirmation(self, event) -> bool:
|
|
481
|
+
buff = event.current_buffer
|
|
482
|
+
text = buff.text
|
|
483
|
+
if self._waiting_for_confirmation and self._confirmation_future:
|
|
484
|
+
# Echo the user input
|
|
485
|
+
self.append_to_output(text + "\n")
|
|
486
|
+
if not self._confirmation_future.done():
|
|
487
|
+
self._confirmation_future.set_result(text)
|
|
488
|
+
buff.reset()
|
|
489
|
+
return True
|
|
490
|
+
return False
|
|
491
|
+
|
|
492
|
+
def _handle_exit_command(self, event) -> bool:
|
|
493
|
+
buff = event.current_buffer
|
|
494
|
+
text = buff.text
|
|
495
|
+
if text.strip().lower() in self._exit_commands:
|
|
496
|
+
event.app.exit()
|
|
497
|
+
return True
|
|
498
|
+
return False
|
|
499
|
+
|
|
500
|
+
def _handle_info_command(self, event) -> bool:
|
|
501
|
+
buff = event.current_buffer
|
|
502
|
+
text = buff.text
|
|
503
|
+
if text.strip().lower() in self._info_commands:
|
|
504
|
+
self.append_to_output(stylize_faint(self._get_help_text()))
|
|
505
|
+
buff.reset()
|
|
506
|
+
return True
|
|
507
|
+
return False
|
|
508
|
+
|
|
509
|
+
def _handle_save_command(self, event) -> bool:
|
|
510
|
+
buff = event.current_buffer
|
|
511
|
+
text = buff.text.strip()
|
|
512
|
+
for cmd in self._save_commands:
|
|
513
|
+
prefix = f"{cmd} "
|
|
514
|
+
if text.lower().startswith(prefix):
|
|
515
|
+
name = text[len(prefix) :].strip()
|
|
516
|
+
if not name:
|
|
517
|
+
continue
|
|
518
|
+
try:
|
|
519
|
+
history = self._history_manager.load(
|
|
520
|
+
self._conversation_session_name
|
|
521
|
+
)
|
|
522
|
+
self._history_manager.update(name, history)
|
|
523
|
+
self._history_manager.save(name)
|
|
524
|
+
self.append_to_output(f"\n š¾ Conversation saved as: {name}\n")
|
|
525
|
+
except Exception as e:
|
|
526
|
+
self.append_to_output(f"\n ā Failed to save conversation: {e}\n")
|
|
527
|
+
buff.reset()
|
|
528
|
+
return True
|
|
529
|
+
return False
|
|
530
|
+
|
|
531
|
+
def _handle_load_command(self, event) -> bool:
|
|
532
|
+
buff = event.current_buffer
|
|
533
|
+
text = buff.text.strip()
|
|
534
|
+
for cmd in self._load_commands:
|
|
535
|
+
prefix = f"{cmd} "
|
|
536
|
+
if text.lower().startswith(prefix):
|
|
537
|
+
name = text[len(prefix) :].strip()
|
|
538
|
+
if not name:
|
|
539
|
+
continue
|
|
540
|
+
self._conversation_session_name = name
|
|
541
|
+
self.append_to_output(
|
|
542
|
+
f"\n š Conversation session switched to: {name}\n"
|
|
543
|
+
)
|
|
544
|
+
buff.reset()
|
|
545
|
+
return True
|
|
546
|
+
return False
|
|
547
|
+
|
|
548
|
+
def _handle_redirect_command(self, event) -> bool:
|
|
549
|
+
buff = event.current_buffer
|
|
550
|
+
text = buff.text.strip()
|
|
551
|
+
for cmd in self._redirect_output_commands:
|
|
552
|
+
prefix = f"{cmd} "
|
|
553
|
+
if text.lower().startswith(prefix):
|
|
554
|
+
path = text[len(prefix) :].strip()
|
|
555
|
+
if not path:
|
|
556
|
+
continue
|
|
557
|
+
|
|
558
|
+
content = self.last_output
|
|
559
|
+
if not content:
|
|
560
|
+
self.append_to_output(
|
|
561
|
+
"\n ā No AI response available to redirect.\n"
|
|
562
|
+
)
|
|
563
|
+
buff.reset()
|
|
564
|
+
return True
|
|
565
|
+
|
|
566
|
+
try:
|
|
567
|
+
# Write to file
|
|
568
|
+
expanded_path = os.path.abspath(os.path.expanduser(path))
|
|
569
|
+
os.makedirs(os.path.dirname(expanded_path), exist_ok=True)
|
|
570
|
+
with open(expanded_path, "w", encoding="utf-8") as f:
|
|
571
|
+
f.write(content)
|
|
572
|
+
self.append_to_output(f"\n š Last output redirected to: {path}\n")
|
|
573
|
+
except Exception as e:
|
|
574
|
+
self.append_to_output(f"\n ā Failed to redirect output: {e}\n")
|
|
575
|
+
|
|
576
|
+
buff.reset()
|
|
577
|
+
return True
|
|
578
|
+
return False
|
|
579
|
+
|
|
580
|
+
def _handle_attach_command(self, event) -> bool:
|
|
581
|
+
buff = event.current_buffer
|
|
582
|
+
text = buff.text
|
|
583
|
+
for attach_command in self._attach_commands:
|
|
584
|
+
if text.strip().lower().startswith(f"{attach_command} "):
|
|
585
|
+
path = text.strip()[8:].strip()
|
|
586
|
+
self._submit_attachment(path)
|
|
587
|
+
buff.reset()
|
|
588
|
+
return True
|
|
589
|
+
return False
|
|
590
|
+
|
|
591
|
+
def _submit_attachment(self, path: str):
|
|
592
|
+
# Validate path
|
|
593
|
+
self.append_to_output(f"\n š¢ Attach {path}...\n")
|
|
594
|
+
expanded_path = os.path.abspath(os.path.expanduser(path))
|
|
595
|
+
if not os.path.exists(expanded_path):
|
|
596
|
+
self.append_to_output(f"\n ā File not found: {path}\n")
|
|
597
|
+
return
|
|
598
|
+
if expanded_path not in self._pending_attachments:
|
|
599
|
+
self._pending_attachments.append(expanded_path)
|
|
600
|
+
self.append_to_output(f"\n š Attached: {path}\n")
|
|
601
|
+
else:
|
|
602
|
+
self.append_to_output(f"\n š Already attached: {path}\n")
|
|
603
|
+
|
|
604
|
+
def append_to_output(
|
|
605
|
+
self,
|
|
606
|
+
*values: object,
|
|
607
|
+
sep: str = " ",
|
|
608
|
+
end: str = "\n",
|
|
609
|
+
file: TextIO | None = None,
|
|
610
|
+
flush: bool = False,
|
|
611
|
+
):
|
|
612
|
+
# Helper to safely append to read-only buffer
|
|
613
|
+
current_text = self._output_field.text
|
|
614
|
+
|
|
615
|
+
# Construct the new content
|
|
616
|
+
content = sep.join([str(value) for value in values]) + end
|
|
617
|
+
|
|
618
|
+
# Handle carriage returns (\r) for status updates
|
|
619
|
+
if "\r" in content:
|
|
620
|
+
# Find the start of the last line in the current text
|
|
621
|
+
last_newline = current_text.rfind("\n")
|
|
622
|
+
if last_newline == -1:
|
|
623
|
+
previous = ""
|
|
624
|
+
last = current_text
|
|
625
|
+
else:
|
|
626
|
+
previous = current_text[: last_newline + 1]
|
|
627
|
+
last = current_text[last_newline + 1 :]
|
|
628
|
+
|
|
629
|
+
combined = last + content
|
|
630
|
+
# Remove content before \r on the same line
|
|
631
|
+
# [^\n]* matches any character except newline
|
|
632
|
+
resolved = re.sub(r"[^\n]*\r", "", combined)
|
|
633
|
+
|
|
634
|
+
new_text = previous + resolved
|
|
635
|
+
else:
|
|
636
|
+
new_text = current_text + content
|
|
637
|
+
|
|
638
|
+
# Update content directly
|
|
639
|
+
# We use bypass_readonly=True by constructing a Document
|
|
640
|
+
self._output_field.buffer.set_document(
|
|
641
|
+
Document(new_text, cursor_position=len(new_text)), bypass_readonly=True
|
|
642
|
+
)
|
|
643
|
+
get_app().invalidate()
|
|
644
|
+
|
|
645
|
+
def _submit_user_message(self, llm_task: AnyTask, user_message: str):
|
|
646
|
+
timestamp = datetime.now().strftime("%H:%M")
|
|
647
|
+
# 1. Render User Message
|
|
648
|
+
self.append_to_output(f"\nš¬ {timestamp} >>\n{user_message.strip()}\n")
|
|
649
|
+
# 2. Trigger AI Response
|
|
650
|
+
attachments = list(self._pending_attachments)
|
|
651
|
+
self._pending_attachments.clear()
|
|
652
|
+
self._running_llm_task = asyncio.create_task(
|
|
653
|
+
self._stream_ai_response(llm_task, user_message, attachments)
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
async def _stream_ai_response(
|
|
657
|
+
self,
|
|
658
|
+
llm_task: AnyTask,
|
|
659
|
+
user_message: str,
|
|
660
|
+
attachments: "list[UserContent]" = [],
|
|
661
|
+
):
|
|
662
|
+
from zrb.llm.agent.agent import tool_confirmation_var
|
|
663
|
+
|
|
664
|
+
self._is_thinking = True
|
|
665
|
+
get_app().invalidate() # Update status bar
|
|
666
|
+
try:
|
|
667
|
+
timestamp = datetime.now().strftime("%H:%M")
|
|
668
|
+
# Header first
|
|
669
|
+
self.append_to_output(f"\nš¤ {timestamp} >>\n")
|
|
670
|
+
session = self._create_sesion_for_llm_task(user_message, attachments)
|
|
671
|
+
|
|
672
|
+
# Run the task with stdout/stderr redirected to UI
|
|
673
|
+
self.append_to_output("\n š¢ Streaming response...\n")
|
|
674
|
+
stdout_capture = StreamToUI(self.append_to_output)
|
|
675
|
+
|
|
676
|
+
# Set context var for tool confirmation
|
|
677
|
+
token = tool_confirmation_var.set(self._confirm_tool_execution)
|
|
678
|
+
try:
|
|
679
|
+
with contextlib.redirect_stdout(
|
|
680
|
+
stdout_capture
|
|
681
|
+
), contextlib.redirect_stderr(stdout_capture):
|
|
682
|
+
result_data = await llm_task.async_run(session)
|
|
683
|
+
finally:
|
|
684
|
+
tool_confirmation_var.reset(token)
|
|
685
|
+
|
|
686
|
+
# Check for final text output
|
|
687
|
+
if result_data is not None:
|
|
688
|
+
if isinstance(result_data, str):
|
|
689
|
+
self._last_result_data = result_data
|
|
690
|
+
width = self._get_output_field_width()
|
|
691
|
+
self.append_to_output("\n")
|
|
692
|
+
self.append_to_output(
|
|
693
|
+
render_markdown(
|
|
694
|
+
result_data, width=width, theme=self._markdown_theme
|
|
695
|
+
)
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
except asyncio.CancelledError:
|
|
699
|
+
self.append_to_output("\n[Cancelled]\n")
|
|
700
|
+
except Exception as e:
|
|
701
|
+
with open("zrb_debug.log", "a") as f:
|
|
702
|
+
f.write(f"[{datetime.now()}] Error: {e}\n")
|
|
703
|
+
self.append_to_output(f"\n[Error: {e}]\n")
|
|
704
|
+
finally:
|
|
705
|
+
self._is_thinking = False
|
|
706
|
+
self._running_llm_task = None
|
|
707
|
+
get_app().invalidate()
|
|
708
|
+
|
|
709
|
+
def _create_sesion_for_llm_task(
|
|
710
|
+
self, user_message: str, attachments: "list[UserContent]"
|
|
711
|
+
) -> AnySession:
|
|
712
|
+
"""Create session to run LLMTask"""
|
|
713
|
+
session_input = {
|
|
714
|
+
"message": user_message,
|
|
715
|
+
"session": self._conversation_session_name,
|
|
716
|
+
"yolo": self._yolo,
|
|
717
|
+
"attachments": attachments,
|
|
718
|
+
}
|
|
719
|
+
shared_ctx = SharedContext(
|
|
720
|
+
input=session_input,
|
|
721
|
+
print_fn=self.append_to_output,
|
|
722
|
+
is_web_mode=True,
|
|
723
|
+
)
|
|
724
|
+
return Session(shared_ctx)
|
|
725
|
+
|
|
726
|
+
def _get_output_field_width(self) -> int | None:
|
|
727
|
+
try:
|
|
728
|
+
width = get_app().output.get_size().columns - 4
|
|
729
|
+
if width < 10:
|
|
730
|
+
width = None
|
|
731
|
+
except Exception:
|
|
732
|
+
width = None
|
|
733
|
+
return width
|