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.

Files changed (204) hide show
  1. zrb/__init__.py +118 -133
  2. zrb/attr/type.py +10 -7
  3. zrb/builtin/__init__.py +55 -1
  4. zrb/builtin/git.py +12 -1
  5. zrb/builtin/group.py +31 -15
  6. zrb/builtin/llm/chat.py +147 -0
  7. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +7 -7
  8. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +5 -5
  9. zrb/builtin/project/add/fastapp/fastapp_util.py +1 -1
  10. zrb/builtin/searxng/config/settings.yml +5671 -0
  11. zrb/builtin/searxng/start.py +21 -0
  12. zrb/builtin/shell/autocomplete/bash.py +4 -3
  13. zrb/builtin/shell/autocomplete/zsh.py +4 -3
  14. zrb/callback/callback.py +8 -1
  15. zrb/cmd/cmd_result.py +2 -1
  16. zrb/config/config.py +555 -169
  17. zrb/config/helper.py +84 -0
  18. zrb/config/web_auth_config.py +50 -35
  19. zrb/context/any_shared_context.py +20 -3
  20. zrb/context/context.py +39 -5
  21. zrb/context/print_fn.py +13 -0
  22. zrb/context/shared_context.py +17 -8
  23. zrb/group/any_group.py +3 -3
  24. zrb/group/group.py +3 -3
  25. zrb/input/any_input.py +5 -1
  26. zrb/input/base_input.py +18 -6
  27. zrb/input/option_input.py +41 -1
  28. zrb/input/text_input.py +7 -24
  29. zrb/llm/agent/__init__.py +9 -0
  30. zrb/llm/agent/agent.py +215 -0
  31. zrb/llm/agent/summarizer.py +20 -0
  32. zrb/llm/app/__init__.py +10 -0
  33. zrb/llm/app/completion.py +281 -0
  34. zrb/llm/app/confirmation/allow_tool.py +66 -0
  35. zrb/llm/app/confirmation/handler.py +178 -0
  36. zrb/llm/app/confirmation/replace_confirmation.py +77 -0
  37. zrb/llm/app/keybinding.py +34 -0
  38. zrb/llm/app/layout.py +117 -0
  39. zrb/llm/app/lexer.py +155 -0
  40. zrb/llm/app/redirection.py +28 -0
  41. zrb/llm/app/style.py +16 -0
  42. zrb/llm/app/ui.py +733 -0
  43. zrb/llm/config/__init__.py +4 -0
  44. zrb/llm/config/config.py +122 -0
  45. zrb/llm/config/limiter.py +247 -0
  46. zrb/llm/history_manager/__init__.py +4 -0
  47. zrb/llm/history_manager/any_history_manager.py +23 -0
  48. zrb/llm/history_manager/file_history_manager.py +91 -0
  49. zrb/llm/history_processor/summarizer.py +108 -0
  50. zrb/llm/note/__init__.py +3 -0
  51. zrb/llm/note/manager.py +122 -0
  52. zrb/llm/prompt/__init__.py +29 -0
  53. zrb/llm/prompt/claude_compatibility.py +92 -0
  54. zrb/llm/prompt/compose.py +55 -0
  55. zrb/llm/prompt/default.py +51 -0
  56. zrb/llm/prompt/markdown/file_extractor.md +112 -0
  57. zrb/llm/prompt/markdown/mandate.md +23 -0
  58. zrb/llm/prompt/markdown/persona.md +3 -0
  59. zrb/llm/prompt/markdown/repo_extractor.md +112 -0
  60. zrb/llm/prompt/markdown/repo_summarizer.md +29 -0
  61. zrb/llm/prompt/markdown/summarizer.md +21 -0
  62. zrb/llm/prompt/note.py +41 -0
  63. zrb/llm/prompt/system_context.py +46 -0
  64. zrb/llm/prompt/zrb.py +41 -0
  65. zrb/llm/skill/__init__.py +3 -0
  66. zrb/llm/skill/manager.py +86 -0
  67. zrb/llm/task/__init__.py +4 -0
  68. zrb/llm/task/llm_chat_task.py +316 -0
  69. zrb/llm/task/llm_task.py +245 -0
  70. zrb/llm/tool/__init__.py +39 -0
  71. zrb/llm/tool/bash.py +75 -0
  72. zrb/llm/tool/code.py +266 -0
  73. zrb/llm/tool/file.py +419 -0
  74. zrb/llm/tool/note.py +70 -0
  75. zrb/{builtin/llm → llm}/tool/rag.py +33 -37
  76. zrb/llm/tool/search/brave.py +53 -0
  77. zrb/llm/tool/search/searxng.py +47 -0
  78. zrb/llm/tool/search/serpapi.py +47 -0
  79. zrb/llm/tool/skill.py +19 -0
  80. zrb/llm/tool/sub_agent.py +70 -0
  81. zrb/llm/tool/web.py +97 -0
  82. zrb/llm/tool/zrb_task.py +66 -0
  83. zrb/llm/util/attachment.py +101 -0
  84. zrb/llm/util/prompt.py +104 -0
  85. zrb/llm/util/stream_response.py +178 -0
  86. zrb/runner/cli.py +21 -20
  87. zrb/runner/common_util.py +24 -19
  88. zrb/runner/web_route/task_input_api_route.py +5 -5
  89. zrb/runner/web_util/user.py +7 -3
  90. zrb/session/any_session.py +12 -9
  91. zrb/session/session.py +38 -17
  92. zrb/task/any_task.py +24 -3
  93. zrb/task/base/context.py +42 -22
  94. zrb/task/base/execution.py +67 -55
  95. zrb/task/base/lifecycle.py +14 -7
  96. zrb/task/base/monitoring.py +12 -7
  97. zrb/task/base_task.py +113 -50
  98. zrb/task/base_trigger.py +16 -6
  99. zrb/task/cmd_task.py +6 -0
  100. zrb/task/http_check.py +11 -5
  101. zrb/task/make_task.py +5 -3
  102. zrb/task/rsync_task.py +30 -10
  103. zrb/task/scaffolder.py +7 -4
  104. zrb/task/scheduler.py +7 -4
  105. zrb/task/tcp_check.py +6 -4
  106. zrb/util/ascii_art/art/bee.txt +17 -0
  107. zrb/util/ascii_art/art/cat.txt +9 -0
  108. zrb/util/ascii_art/art/ghost.txt +16 -0
  109. zrb/util/ascii_art/art/panda.txt +17 -0
  110. zrb/util/ascii_art/art/rose.txt +14 -0
  111. zrb/util/ascii_art/art/unicorn.txt +15 -0
  112. zrb/util/ascii_art/banner.py +92 -0
  113. zrb/util/attr.py +54 -39
  114. zrb/util/cli/markdown.py +32 -0
  115. zrb/util/cli/text.py +30 -0
  116. zrb/util/cmd/command.py +33 -10
  117. zrb/util/file.py +61 -33
  118. zrb/util/git.py +2 -2
  119. zrb/util/{llm/prompt.py → markdown.py} +2 -3
  120. zrb/util/match.py +78 -0
  121. zrb/util/run.py +3 -3
  122. zrb/util/string/conversion.py +1 -1
  123. zrb/util/truncate.py +23 -0
  124. zrb/util/yaml.py +204 -0
  125. zrb/xcom/xcom.py +10 -0
  126. {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/METADATA +41 -27
  127. {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/RECORD +129 -131
  128. {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/WHEEL +1 -1
  129. zrb/attr/__init__.py +0 -0
  130. zrb/builtin/llm/chat_session.py +0 -311
  131. zrb/builtin/llm/history.py +0 -71
  132. zrb/builtin/llm/input.py +0 -27
  133. zrb/builtin/llm/llm_ask.py +0 -187
  134. zrb/builtin/llm/previous-session.js +0 -21
  135. zrb/builtin/llm/tool/__init__.py +0 -0
  136. zrb/builtin/llm/tool/api.py +0 -71
  137. zrb/builtin/llm/tool/cli.py +0 -38
  138. zrb/builtin/llm/tool/code.py +0 -254
  139. zrb/builtin/llm/tool/file.py +0 -626
  140. zrb/builtin/llm/tool/sub_agent.py +0 -137
  141. zrb/builtin/llm/tool/web.py +0 -195
  142. zrb/builtin/project/__init__.py +0 -0
  143. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/__init__.py +0 -0
  144. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/service/__init__.py +0 -0
  145. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/__init__.py +0 -0
  146. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/__init__.py +0 -0
  147. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/__init__.py +0 -0
  148. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/__init__.py +0 -0
  149. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/__init__.py +0 -0
  150. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/__init__.py +0 -0
  151. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/__init__.py +0 -0
  152. zrb/builtin/project/create/__init__.py +0 -0
  153. zrb/builtin/shell/__init__.py +0 -0
  154. zrb/builtin/shell/autocomplete/__init__.py +0 -0
  155. zrb/callback/__init__.py +0 -0
  156. zrb/cmd/__init__.py +0 -0
  157. zrb/config/default_prompt/file_extractor_system_prompt.md +0 -12
  158. zrb/config/default_prompt/interactive_system_prompt.md +0 -35
  159. zrb/config/default_prompt/persona.md +0 -1
  160. zrb/config/default_prompt/repo_extractor_system_prompt.md +0 -112
  161. zrb/config/default_prompt/repo_summarizer_system_prompt.md +0 -10
  162. zrb/config/default_prompt/summarization_prompt.md +0 -16
  163. zrb/config/default_prompt/system_prompt.md +0 -32
  164. zrb/config/llm_config.py +0 -243
  165. zrb/config/llm_context/config.py +0 -129
  166. zrb/config/llm_context/config_parser.py +0 -46
  167. zrb/config/llm_rate_limitter.py +0 -137
  168. zrb/content_transformer/__init__.py +0 -0
  169. zrb/context/__init__.py +0 -0
  170. zrb/dot_dict/__init__.py +0 -0
  171. zrb/env/__init__.py +0 -0
  172. zrb/group/__init__.py +0 -0
  173. zrb/input/__init__.py +0 -0
  174. zrb/runner/__init__.py +0 -0
  175. zrb/runner/web_route/__init__.py +0 -0
  176. zrb/runner/web_route/home_page/__init__.py +0 -0
  177. zrb/session/__init__.py +0 -0
  178. zrb/session_state_log/__init__.py +0 -0
  179. zrb/session_state_logger/__init__.py +0 -0
  180. zrb/task/__init__.py +0 -0
  181. zrb/task/base/__init__.py +0 -0
  182. zrb/task/llm/__init__.py +0 -0
  183. zrb/task/llm/agent.py +0 -243
  184. zrb/task/llm/config.py +0 -103
  185. zrb/task/llm/conversation_history.py +0 -128
  186. zrb/task/llm/conversation_history_model.py +0 -242
  187. zrb/task/llm/default_workflow/coding.md +0 -24
  188. zrb/task/llm/default_workflow/copywriting.md +0 -17
  189. zrb/task/llm/default_workflow/researching.md +0 -18
  190. zrb/task/llm/error.py +0 -95
  191. zrb/task/llm/history_summarization.py +0 -216
  192. zrb/task/llm/print_node.py +0 -101
  193. zrb/task/llm/prompt.py +0 -325
  194. zrb/task/llm/tool_wrapper.py +0 -220
  195. zrb/task/llm/typing.py +0 -3
  196. zrb/task/llm_task.py +0 -341
  197. zrb/task_status/__init__.py +0 -0
  198. zrb/util/__init__.py +0 -0
  199. zrb/util/cli/__init__.py +0 -0
  200. zrb/util/cmd/__init__.py +0 -0
  201. zrb/util/codemod/__init__.py +0 -0
  202. zrb/util/string/__init__.py +0 -0
  203. zrb/xcom/__init__.py +0 -0
  204. {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,178 @@
1
+ import json
2
+ import os
3
+ import subprocess
4
+ import tempfile
5
+ from typing import Any, Awaitable, Callable, Protocol, TextIO
6
+
7
+ import yaml
8
+
9
+ from zrb.config.config import CFG
10
+ from zrb.util.yaml import yaml_dump
11
+
12
+
13
+ class UIProtocol(Protocol):
14
+ async def ask_user(self, prompt: str) -> str: ...
15
+
16
+ def append_to_output(
17
+ self,
18
+ *values: object,
19
+ sep: str = " ",
20
+ end: str = "\n",
21
+ file: TextIO | None = None,
22
+ flush: bool = False,
23
+ ): ...
24
+
25
+
26
+ ConfirmationMiddleware = Callable[
27
+ [UIProtocol, Any, str, Callable[[UIProtocol, Any, str], Awaitable[Any]]],
28
+ Awaitable[Any],
29
+ ]
30
+
31
+
32
+ class ConfirmationHandler:
33
+ def __init__(self, middlewares: list[ConfirmationMiddleware]):
34
+ self._middlewares = middlewares
35
+
36
+ def add_middleware(self, *middleware: ConfirmationMiddleware):
37
+ self.prepend_middleware(*middleware)
38
+
39
+ def prepend_middleware(self, *middleware: ConfirmationMiddleware):
40
+ self._middlewares = list(middleware) + self._middlewares
41
+
42
+ async def handle(self, ui: UIProtocol, call: Any) -> Any:
43
+ while True:
44
+ message = self._get_confirm_user_message(call)
45
+ ui.append_to_output(f"\n\n{message}", end="")
46
+ # Wait for user input
47
+ user_input = await ui.ask_user("")
48
+ user_response = user_input.strip()
49
+
50
+ # Build the chain
51
+ async def _next(
52
+ ui: UIProtocol, call: Any, response: str, index: int
53
+ ) -> Any:
54
+ if index >= len(self._middlewares):
55
+ # Default if no middleware handles it
56
+ return None
57
+ middleware = self._middlewares[index]
58
+ return await middleware(
59
+ ui,
60
+ call,
61
+ response,
62
+ lambda u, c, r: _next(u, c, r, index + 1),
63
+ )
64
+
65
+ result = await _next(ui, call, user_response, 0)
66
+ if result is None:
67
+ continue
68
+ return result
69
+
70
+ def _get_confirm_user_message(self, call: Any) -> str:
71
+ args_section = ""
72
+ if f"{call.args}" != "{}":
73
+ args_str = self._format_args(call.args)
74
+ args_section = f" Arguments:\n{args_str}\n"
75
+ return (
76
+ f" 🎰 Executing tool '{call.tool_name}'\n"
77
+ f"{args_section}"
78
+ " ❓ Allow tool Execution? (✅ Y | 🛑 n | ✏️ e)? "
79
+ )
80
+
81
+ def _format_args(self, args: Any) -> str:
82
+ indent = " " * 7
83
+ try:
84
+ if isinstance(args, str):
85
+ try:
86
+ args = json.loads(args)
87
+ except json.JSONDecodeError:
88
+ pass
89
+ args_str = yaml_dump(args)
90
+ # Indent nicely for display
91
+ return "\n".join([f"{indent}{line}" for line in args_str.splitlines()])
92
+ except Exception:
93
+ return f"{indent}{args}"
94
+
95
+
96
+ async def last_confirmation(
97
+ ui: UIProtocol,
98
+ call: Any,
99
+ user_response: str,
100
+ next_handler: Callable[[UIProtocol, Any, str], Awaitable[Any]],
101
+ ) -> Any:
102
+ from pydantic_ai import ToolApproved, ToolDenied
103
+
104
+ if user_response.lower() in ("y", "yes", "ok", "okay", ""):
105
+ ui.append_to_output("\n✅ Execution approved.")
106
+ return ToolApproved()
107
+ elif user_response.lower() in ("n", "no"):
108
+ ui.append_to_output("\n🛑 Execution denied.")
109
+ return ToolDenied("User denied execution")
110
+ elif user_response.lower() in ("e", "edit"):
111
+ # Edit logic
112
+ try:
113
+ args = call.args
114
+ if isinstance(args, str):
115
+ try:
116
+ args = json.loads(args)
117
+ except json.JSONDecodeError:
118
+ pass
119
+
120
+ # YAML for editing
121
+ is_yaml_edit = True
122
+ try:
123
+ content = yaml_dump(args)
124
+ extension = ".yaml"
125
+ except Exception:
126
+ # Fallback to JSON
127
+ content = json.dumps(args, indent=2)
128
+ extension = ".json"
129
+ is_yaml_edit = False
130
+
131
+ new_content = await wait_edit_content(
132
+ text_editor=CFG.DEFAULT_EDITOR,
133
+ content=content,
134
+ extension=extension,
135
+ )
136
+
137
+ # Compare content
138
+ if new_content == content:
139
+ ui.append_to_output("\nℹ️ No changes made.")
140
+ return None
141
+
142
+ try:
143
+ if is_yaml_edit:
144
+ new_args = yaml.safe_load(new_content)
145
+ else:
146
+ new_args = json.loads(new_content)
147
+ ui.append_to_output("\n✅ Execution approved (with modification).")
148
+ return ToolApproved(override_args=new_args)
149
+ except Exception as e:
150
+ ui.append_to_output(f"\n❌ Invalid format: {e}. ", end="")
151
+ # Return None to signal loop retry
152
+ return None
153
+
154
+ except Exception as e:
155
+ ui.append_to_output(f"\n❌ Error editing: {e}. ", end="")
156
+ return None
157
+ else:
158
+ ui.append_to_output("\n🛑 Execution denied.")
159
+ return ToolDenied(f"User denied execution with message: {user_response}")
160
+
161
+
162
+ async def wait_edit_content(
163
+ text_editor: str, content: str, extension: str = ".txt"
164
+ ) -> str:
165
+ from prompt_toolkit.application import run_in_terminal
166
+
167
+ # Write temporary file
168
+ with tempfile.NamedTemporaryFile(suffix=extension, mode="w+", delete=False) as tf:
169
+ tf.write(content)
170
+ tf_path = tf.name
171
+
172
+ # Edit and wait
173
+ await run_in_terminal(lambda: subprocess.call([text_editor, tf_path]))
174
+ with open(tf_path, "r") as tf:
175
+ new_content = tf.read()
176
+ os.remove(tf_path)
177
+
178
+ return new_content
@@ -0,0 +1,77 @@
1
+ import json
2
+ import os
3
+ import subprocess
4
+ import tempfile
5
+ from typing import Any, Awaitable, Callable
6
+
7
+ from prompt_toolkit.application import run_in_terminal
8
+
9
+ from zrb.config.config import CFG
10
+ from zrb.llm.app.confirmation.handler import UIProtocol
11
+
12
+
13
+ async def replace_confirmation(
14
+ ui: UIProtocol,
15
+ call: Any,
16
+ response: str,
17
+ next_handler: Callable[[UIProtocol, Any, str], Awaitable[Any]],
18
+ ) -> Any:
19
+ from pydantic_ai import ToolApproved
20
+
21
+ if call.tool_name != "replace_in_file":
22
+ return await next_handler(ui, call, response)
23
+
24
+ if response.lower() not in ("e", "edit"):
25
+ return await next_handler(ui, call, response)
26
+
27
+ # It is replace_in_file and user wants to edit
28
+ args = call.args
29
+ if isinstance(args, str):
30
+ try:
31
+ args = json.loads(args)
32
+ except json.JSONDecodeError:
33
+ pass
34
+
35
+ old_text = args.get("old_text", "")
36
+ new_text = args.get("new_text", "")
37
+
38
+ # Create temporary files
39
+ with tempfile.NamedTemporaryFile(mode="w+", delete=False, suffix=".old") as tf_old:
40
+ tf_old.write(old_text)
41
+ old_path = tf_old.name
42
+
43
+ with tempfile.NamedTemporaryFile(mode="w+", delete=False, suffix=".new") as tf_new:
44
+ tf_new.write(new_text)
45
+ new_path = tf_new.name
46
+
47
+ try:
48
+ # Prepare command
49
+ cmd_tpl = CFG.DEFAULT_DIFF_EDIT_COMMAND_TPL
50
+ cmd = cmd_tpl.format(old=old_path, new=new_path)
51
+
52
+ # Run command
53
+ await run_in_terminal(lambda: subprocess.call(cmd, shell=True))
54
+
55
+ # Read back new content
56
+ with open(new_path, "r") as f:
57
+ edited_new_text = f.read()
58
+
59
+ if edited_new_text != new_text:
60
+ # Update args
61
+ new_args = dict(args)
62
+ new_args["new_text"] = edited_new_text
63
+ ui.append_to_output("\n✅ Replacement modified.")
64
+ return ToolApproved(override_args=new_args)
65
+ else:
66
+ ui.append_to_output("\nℹ️ No changes made.")
67
+ return None
68
+
69
+ except Exception as e:
70
+ ui.append_to_output(f"\n❌ Error during diff edit: {e}")
71
+ return None
72
+ finally:
73
+ # Cleanup
74
+ if os.path.exists(old_path):
75
+ os.remove(old_path)
76
+ if os.path.exists(new_path):
77
+ os.remove(new_path)
@@ -0,0 +1,34 @@
1
+ import string
2
+
3
+ from prompt_toolkit.application import get_app
4
+ from prompt_toolkit.key_binding import KeyBindings
5
+ from prompt_toolkit.widgets import TextArea
6
+
7
+
8
+ def create_output_keybindings(input_field: TextArea) -> KeyBindings:
9
+ kb = KeyBindings()
10
+
11
+ @kb.add("escape")
12
+ def _(event):
13
+ get_app().layout.focus(input_field)
14
+
15
+ @kb.add("c-c")
16
+ def _(event):
17
+ # Copy selection to clipboard
18
+ if event.current_buffer.selection_state:
19
+ data = event.current_buffer.copy_selection()
20
+ event.app.clipboard.set_data(data)
21
+ get_app().layout.focus(input_field)
22
+
23
+ def redirect_focus(event):
24
+ get_app().layout.focus(input_field)
25
+ input_field.buffer.insert_text(event.data)
26
+
27
+ for char in string.printable:
28
+ # Skip control characters (Tab, Newline, etc.)
29
+ # to preserve navigation/standard behavior
30
+ if char in "\t\n\r\x0b\x0c":
31
+ continue
32
+ kb.add(char)(redirect_focus)
33
+
34
+ return kb
zrb/llm/app/layout.py ADDED
@@ -0,0 +1,117 @@
1
+ from typing import Callable
2
+
3
+ from prompt_toolkit.formatted_text import HTML, AnyFormattedText
4
+ from prompt_toolkit.layout import HSplit, Layout, Window, WindowAlign
5
+ from prompt_toolkit.layout.containers import Float, FloatContainer
6
+ from prompt_toolkit.layout.controls import FormattedTextControl
7
+ from prompt_toolkit.layout.menus import CompletionsMenu
8
+ from prompt_toolkit.lexers import Lexer
9
+ from prompt_toolkit.widgets import Frame, TextArea
10
+
11
+ from zrb.llm.app.completion import InputCompleter
12
+ from zrb.llm.history_manager.any_history_manager import AnyHistoryManager
13
+
14
+
15
+ def create_input_field(
16
+ history_manager: AnyHistoryManager,
17
+ attach_commands: list[str],
18
+ exit_commands: list[str],
19
+ info_commands: list[str],
20
+ save_commands: list[str],
21
+ load_commands: list[str],
22
+ redirect_output_commands: list[str],
23
+ summarize_commands: list[str],
24
+ exec_commands: list[str],
25
+ ) -> TextArea:
26
+ return TextArea(
27
+ height=4,
28
+ prompt=HTML('<style color="ansibrightblue"><b>&gt;&gt;&gt; </b></style>'),
29
+ multiline=True,
30
+ wrap_lines=True,
31
+ completer=InputCompleter(
32
+ history_manager=history_manager,
33
+ attach_commands=attach_commands,
34
+ exit_commands=exit_commands,
35
+ info_commands=info_commands,
36
+ save_commands=save_commands,
37
+ load_commands=load_commands,
38
+ redirect_output_commands=redirect_output_commands,
39
+ summarize_commands=summarize_commands,
40
+ exec_commands=exec_commands,
41
+ ),
42
+ complete_while_typing=True,
43
+ focus_on_click=True,
44
+ style="class:input_field",
45
+ )
46
+
47
+
48
+ def create_output_field(greeting: str, lexer: Lexer) -> TextArea:
49
+ return TextArea(
50
+ text=greeting.rstrip() + "\n\n",
51
+ read_only=True,
52
+ scrollbar=False,
53
+ wrap_lines=True,
54
+ lexer=lexer,
55
+ focus_on_click=True,
56
+ focusable=True,
57
+ style="class:output_field",
58
+ )
59
+
60
+
61
+ def create_layout(
62
+ title: str,
63
+ jargon: str,
64
+ input_field: TextArea,
65
+ output_field: TextArea,
66
+ info_bar_text: Callable[[], AnyFormattedText],
67
+ status_bar_text: Callable[[], AnyFormattedText],
68
+ ) -> Layout:
69
+ title_bar_text = HTML(
70
+ f" <style bg='ansipurple' color='white'><b> {title} </b></style> "
71
+ f"<style color='#888888'>| {jargon}</style>"
72
+ )
73
+
74
+ return Layout(
75
+ FloatContainer(
76
+ content=HSplit(
77
+ [
78
+ # Title Bar
79
+ Window(
80
+ height=2,
81
+ content=FormattedTextControl(title_bar_text),
82
+ style="class:title-bar",
83
+ align=WindowAlign.CENTER,
84
+ ),
85
+ # Info Bar
86
+ Window(
87
+ height=2,
88
+ content=FormattedTextControl(info_bar_text),
89
+ style="class:info-bar",
90
+ align=WindowAlign.CENTER,
91
+ ),
92
+ # Chat History
93
+ Frame(output_field, title="Conversation", style="class:frame"),
94
+ # Input Area
95
+ Frame(
96
+ input_field,
97
+ title="(ENTER to send, CTRL+ENTER for newline, ESC to cancel)",
98
+ style="class:input-frame",
99
+ ),
100
+ # Status Bar
101
+ Window(
102
+ height=1,
103
+ content=FormattedTextControl(status_bar_text),
104
+ style="class:bottom-toolbar",
105
+ ),
106
+ ]
107
+ ),
108
+ floats=[
109
+ Float(
110
+ xcursor=True,
111
+ ycursor=True,
112
+ content=CompletionsMenu(max_height=16, scroll_offset=1),
113
+ ),
114
+ ],
115
+ ),
116
+ focused_element=input_field,
117
+ )
zrb/llm/app/lexer.py ADDED
@@ -0,0 +1,155 @@
1
+ import re
2
+
3
+ from prompt_toolkit.lexers import Lexer
4
+
5
+
6
+ class CLIStyleLexer(Lexer):
7
+ def lex_document(self, document):
8
+ lines = document.lines
9
+ line_tokens = {} # Cache for tokens per line
10
+
11
+ # Global state for the document
12
+ current_attrs = set()
13
+ current_fg = ""
14
+ current_bg = ""
15
+
16
+ # Pre-process all lines to handle state across newlines
17
+ # Regex to find ANSI escape sequences (CSI)
18
+ ansi_escape = re.compile(r"\x1B\[([0-9;]*)m")
19
+
20
+ for lineno, line in enumerate(lines):
21
+ tokens = []
22
+ last_end = 0
23
+
24
+ def build_style():
25
+ parts = list(current_attrs)
26
+ if current_fg:
27
+ parts.append(current_fg)
28
+ if current_bg:
29
+ parts.append(current_bg)
30
+ return " ".join(parts)
31
+
32
+ for match in ansi_escape.finditer(line):
33
+ start, end = match.span()
34
+
35
+ # Add text before the escape sequence with current style
36
+ if start > last_end:
37
+ tokens.append((build_style(), line[last_end:start]))
38
+
39
+ # Parse codes
40
+ codes = match.group(1).split(";")
41
+ if not codes or codes == [""]:
42
+ codes = ["0"]
43
+
44
+ # Convert to integers
45
+ int_codes = []
46
+ for c in codes:
47
+ if c.isdigit():
48
+ int_codes.append(int(c))
49
+
50
+ i = 0
51
+ while i < len(int_codes):
52
+ c = int_codes[i]
53
+ i += 1
54
+
55
+ if c == 0:
56
+ current_attrs.clear()
57
+ current_fg = ""
58
+ current_bg = ""
59
+ elif c == 1:
60
+ current_attrs.add("bold")
61
+ elif c == 2:
62
+ current_attrs.add("class:faint")
63
+ elif c == 3:
64
+ current_attrs.add("italic")
65
+ elif c == 4:
66
+ current_attrs.add("underline")
67
+ elif c == 22:
68
+ current_attrs.discard("bold")
69
+ current_attrs.discard("class:faint")
70
+ elif c == 23:
71
+ current_attrs.discard("italic")
72
+ elif c == 24:
73
+ current_attrs.discard("underline")
74
+ elif 30 <= c <= 37:
75
+ colors = [
76
+ "#000000",
77
+ "#ff0000",
78
+ "#00ff00",
79
+ "#ffff00",
80
+ "#0000ff",
81
+ "#ff00ff",
82
+ "#00ffff",
83
+ "#ffffff",
84
+ ]
85
+ current_fg = colors[c - 30]
86
+ elif c == 38:
87
+ if i < len(int_codes):
88
+ mode = int_codes[i]
89
+ i += 1
90
+ if mode == 5 and i < len(int_codes):
91
+ i += 1 # Skip 256 color
92
+ elif mode == 2 and i + 2 < len(int_codes):
93
+ r, g, b = (
94
+ int_codes[i],
95
+ int_codes[i + 1],
96
+ int_codes[i + 2],
97
+ )
98
+ i += 3
99
+ current_fg = f"#{r:02x}{g:02x}{b:02x}"
100
+ elif c == 39:
101
+ current_fg = ""
102
+ elif 40 <= c <= 47:
103
+ colors = [
104
+ "#000000",
105
+ "#ff0000",
106
+ "#00ff00",
107
+ "#ffff00",
108
+ "#0000ff",
109
+ "#ff00ff",
110
+ "#00ffff",
111
+ "#ffffff",
112
+ ]
113
+ current_bg = f"bg:{colors[c - 40]}"
114
+ elif c == 48:
115
+ if i < len(int_codes):
116
+ mode = int_codes[i]
117
+ i += 1
118
+ if mode == 5 and i < len(int_codes):
119
+ i += 1
120
+ elif mode == 2 and i + 2 < len(int_codes):
121
+ r, g, b = (
122
+ int_codes[i],
123
+ int_codes[i + 1],
124
+ int_codes[i + 2],
125
+ )
126
+ i += 3
127
+ current_bg = f"bg:#{r:02x}{g:02x}{b:02x}"
128
+ elif c == 49:
129
+ current_bg = ""
130
+ elif 90 <= c <= 97:
131
+ colors = [
132
+ "#555555",
133
+ "#ff5555",
134
+ "#55ff55",
135
+ "#ffff55",
136
+ "#5555ff",
137
+ "#ff55ff",
138
+ "#55ffff",
139
+ "#ffffff",
140
+ ]
141
+ current_fg = colors[c - 90]
142
+
143
+ last_end = end
144
+
145
+ # Add remaining text
146
+ if last_end < len(line):
147
+ tokens.append((build_style(), line[last_end:]))
148
+
149
+ # Store tokens for this line
150
+ line_tokens[lineno] = tokens
151
+
152
+ def get_line(lineno):
153
+ return line_tokens.get(lineno, [])
154
+
155
+ return get_line
@@ -0,0 +1,28 @@
1
+ import io
2
+ import sys
3
+
4
+
5
+ class StreamToUI(io.TextIOBase):
6
+ """Redirect stdout to UI's append_to_output."""
7
+
8
+ def __init__(self, ui_callback):
9
+ self.ui_callback = ui_callback
10
+ self.original_stdout = sys.stdout
11
+ self.original_stderr = sys.stderr
12
+ self._is_first_write = True
13
+
14
+ def write(self, text: str) -> int:
15
+ from prompt_toolkit.application import get_app
16
+
17
+ text = text.expandtabs(4)
18
+ if text:
19
+ if self._is_first_write:
20
+ self.ui_callback("\n", end="")
21
+ self._is_first_write = False
22
+ self.ui_callback(text, end="")
23
+ get_app().invalidate()
24
+ return len(text)
25
+
26
+ def flush(self):
27
+ self.original_stdout.flush()
28
+ self.original_stderr.flush()
zrb/llm/app/style.py ADDED
@@ -0,0 +1,16 @@
1
+ from prompt_toolkit.styles import Style
2
+
3
+
4
+ def create_style() -> Style:
5
+ return Style.from_dict(
6
+ {
7
+ "frame.label": "bg:#000000 #ffff00",
8
+ "thinking": "ansigreen italic",
9
+ "faint": "#888888",
10
+ "output_field": "bg:#000000 #eeeeee",
11
+ "input_field": "bg:#000000 #eeeeee",
12
+ "text": "#eeeeee",
13
+ "status": "reverse",
14
+ "bottom-toolbar": "bg:#333333 #aaaaaa",
15
+ }
16
+ )