ommlds 0.0.0.dev490__py3-none-any.whl → 0.0.0.dev491__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.
- ommlds/.omlish-manifests.json +3 -3
- ommlds/README.md +11 -0
- ommlds/__about__.py +1 -1
- ommlds/backends/ollama/_dataclasses.py +53 -23
- ommlds/backends/ollama/protocol.py +3 -0
- ommlds/cli/_dataclasses.py +439 -289
- ommlds/cli/main.py +40 -33
- ommlds/cli/rendering/types.py +6 -0
- ommlds/cli/sessions/chat/configs.py +2 -2
- ommlds/cli/sessions/chat/{agents → drivers}/ai/inject.py +3 -1
- ommlds/cli/sessions/chat/{agents → drivers}/configs.py +1 -1
- ommlds/cli/sessions/chat/{agents/agent.py → drivers/driver.py} +1 -1
- ommlds/cli/sessions/chat/{agents → drivers}/inject.py +13 -6
- ommlds/cli/sessions/chat/{agents → drivers}/tools/configs.py +0 -2
- ommlds/cli/sessions/chat/drivers/tools/confirmation.py +44 -0
- ommlds/cli/sessions/chat/{agents → drivers}/tools/execution.py +2 -3
- ommlds/cli/sessions/chat/{agents → drivers}/tools/inject.py +1 -13
- ommlds/cli/sessions/chat/{agents → drivers}/tools/rendering.py +1 -1
- ommlds/cli/sessions/chat/drivers/types.py +10 -0
- ommlds/cli/sessions/chat/{agents → drivers}/user/inject.py +3 -3
- ommlds/cli/sessions/chat/inject.py +2 -2
- ommlds/cli/sessions/chat/interfaces/bare/inject.py +23 -0
- ommlds/cli/sessions/chat/interfaces/bare/interactive.py +6 -6
- ommlds/cli/sessions/chat/interfaces/bare/oneshot.py +5 -5
- ommlds/cli/sessions/chat/{agents/tools/confirmation.py → interfaces/bare/tools.py} +2 -21
- ommlds/cli/sessions/chat/interfaces/bare/user.py +1 -1
- ommlds/cli/sessions/chat/interfaces/configs.py +8 -0
- ommlds/cli/sessions/chat/interfaces/textual/app.py +100 -94
- ommlds/cli/sessions/chat/interfaces/textual/inject.py +29 -5
- ommlds/cli/sessions/chat/interfaces/textual/styles/__init__.py +29 -0
- ommlds/cli/sessions/chat/interfaces/textual/styles/markdown.tcss +7 -0
- ommlds/cli/sessions/chat/interfaces/textual/styles/messages.tcss +72 -1
- ommlds/cli/sessions/chat/interfaces/textual/widgets/__init__.py +0 -0
- ommlds/cli/sessions/chat/interfaces/textual/widgets/input.py +36 -0
- ommlds/cli/sessions/chat/interfaces/textual/widgets/messages.py +114 -0
- ommlds/minichain/backends/impls/ollama/chat.py +24 -56
- ommlds/minichain/backends/impls/ollama/protocol.py +144 -0
- ommlds/nanochat/rustbpe/README.md +9 -0
- {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev491.dist-info}/METADATA +6 -6
- {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev491.dist-info}/RECORD +83 -74
- /ommlds/cli/sessions/chat/{agents → drivers}/__init__.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/ai/__init__.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/ai/configs.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/ai/events.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/ai/injection.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/ai/rendering.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/ai/services.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/ai/tools.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/ai/types.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/events/__init__.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/events/inject.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/events/injection.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/events/manager.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/events/types.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/phases/__init__.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/phases/inject.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/phases/injection.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/phases/manager.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/phases/types.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/state/__init__.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/state/configs.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/state/inject.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/state/inmemory.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/state/storage.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/state/types.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/tools/__init__.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/tools/fs/__init__.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/tools/fs/configs.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/tools/fs/inject.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/tools/injection.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/tools/todo/__init__.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/tools/todo/configs.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/tools/todo/inject.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/tools/weather/__init__.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/tools/weather/configs.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/tools/weather/inject.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/tools/weather/tools.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/user/__init__.py +0 -0
- /ommlds/cli/sessions/chat/{agents → drivers}/user/configs.py +0 -0
- {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev491.dist-info}/WHEEL +0 -0
- {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev491.dist-info}/entry_points.txt +0 -0
- {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev491.dist-info}/licenses/LICENSE +0 -0
- {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev491.dist-info}/top_level.txt +0 -0
|
@@ -1,114 +1,50 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import dataclasses as dc
|
|
3
|
-
import io
|
|
4
2
|
import typing as ta
|
|
5
3
|
|
|
6
4
|
from omdev.tui import textual as tx
|
|
7
5
|
from omlish import check
|
|
8
|
-
from omlish import lang
|
|
9
6
|
|
|
10
7
|
from ...... import minichain as mc
|
|
11
|
-
from ...
|
|
12
|
-
from ...
|
|
8
|
+
from ...drivers.driver import ChatDriver
|
|
9
|
+
from ...drivers.events.types import AiDeltaChatEvent
|
|
10
|
+
from ...drivers.events.types import AiMessagesChatEvent
|
|
11
|
+
from .styles import read_app_css
|
|
12
|
+
from .widgets.input import InputOuter
|
|
13
|
+
from .widgets.input import InputTextArea
|
|
14
|
+
from .widgets.messages import AiMessage
|
|
15
|
+
from .widgets.messages import StaticAiMessage
|
|
16
|
+
from .widgets.messages import StreamAiMessage
|
|
17
|
+
from .widgets.messages import UserMessage
|
|
18
|
+
from .widgets.messages import WelcomeMessage
|
|
13
19
|
|
|
14
20
|
|
|
15
21
|
##
|
|
16
22
|
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
ChatDriverEventQueue = ta.NewType('ChatDriverEventQueue', asyncio.Queue)
|
|
19
25
|
|
|
20
26
|
|
|
21
27
|
##
|
|
22
28
|
|
|
23
29
|
|
|
24
|
-
class WelcomeMessage(tx.Static):
|
|
25
|
-
def __init__(self, content: str) -> None:
|
|
26
|
-
super().__init__(content)
|
|
27
|
-
|
|
28
|
-
self.add_class('welcome-message')
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class UserMessage(tx.Static):
|
|
32
|
-
def __init__(self, content: str) -> None:
|
|
33
|
-
super().__init__(content)
|
|
34
|
-
|
|
35
|
-
self.add_class('user-message')
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class AiMessage(tx.Static):
|
|
39
|
-
def __init__(self, content: str) -> None:
|
|
40
|
-
super().__init__(content)
|
|
41
|
-
|
|
42
|
-
self.add_class('ai-message')
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
##
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class InputTextArea(tx.TextArea):
|
|
49
|
-
@dc.dataclass()
|
|
50
|
-
class Submitted(tx.Message):
|
|
51
|
-
text: str
|
|
52
|
-
|
|
53
|
-
def __init__(self, **kwargs: ta.Any) -> None:
|
|
54
|
-
super().__init__(**kwargs)
|
|
55
|
-
|
|
56
|
-
async def _on_key(self, event: tx.Key) -> None:
|
|
57
|
-
if event.key == 'enter':
|
|
58
|
-
event.prevent_default()
|
|
59
|
-
event.stop()
|
|
60
|
-
|
|
61
|
-
if text := self.text.strip():
|
|
62
|
-
self.post_message(self.Submitted(text))
|
|
63
|
-
|
|
64
|
-
else:
|
|
65
|
-
await super()._on_key(event)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
##
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
@lang.cached_function
|
|
72
|
-
def _read_app_css() -> str:
|
|
73
|
-
tcss_rsrcs = [
|
|
74
|
-
rsrc
|
|
75
|
-
for rsrc in lang.get_relative_resources('.styles', globals=globals()).values()
|
|
76
|
-
if rsrc.name.endswith('.tcss')
|
|
77
|
-
]
|
|
78
|
-
|
|
79
|
-
out = io.StringIO()
|
|
80
|
-
|
|
81
|
-
for i, rsrc in enumerate(tcss_rsrcs):
|
|
82
|
-
if i:
|
|
83
|
-
out.write('\n\n')
|
|
84
|
-
|
|
85
|
-
out.write(f'/* {rsrc.name} */\n')
|
|
86
|
-
out.write('\n')
|
|
87
|
-
|
|
88
|
-
out.write(rsrc.read_text().strip())
|
|
89
|
-
out.write('\n')
|
|
90
|
-
|
|
91
|
-
return out.getvalue()
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
97
30
|
class ChatApp(tx.App):
|
|
31
|
+
ENABLE_COMMAND_PALETTE: ta.ClassVar[bool] = False
|
|
32
|
+
|
|
98
33
|
def __init__(
|
|
99
34
|
self,
|
|
100
35
|
*,
|
|
101
|
-
|
|
102
|
-
event_queue:
|
|
36
|
+
driver: ChatDriver,
|
|
37
|
+
event_queue: ChatDriverEventQueue,
|
|
103
38
|
) -> None:
|
|
104
39
|
super().__init__()
|
|
105
40
|
|
|
106
|
-
self.
|
|
41
|
+
self._chat_driver = driver
|
|
107
42
|
self._event_queue = event_queue
|
|
108
43
|
|
|
109
|
-
|
|
44
|
+
def get_driver_class(self) -> type[tx.Driver]:
|
|
45
|
+
return tx.get_pending_writes_driver_class(super().get_driver_class())
|
|
110
46
|
|
|
111
|
-
|
|
47
|
+
CSS: ta.ClassVar[str] = read_app_css()
|
|
112
48
|
|
|
113
49
|
#
|
|
114
50
|
|
|
@@ -116,23 +52,33 @@ class ChatApp(tx.App):
|
|
|
116
52
|
with tx.VerticalScroll(id='messages-scroll'):
|
|
117
53
|
yield tx.Static(id='messages-container')
|
|
118
54
|
|
|
119
|
-
|
|
120
|
-
with tx.Vertical(id='input-vertical'):
|
|
121
|
-
with tx.Vertical(id='input-vertical2'):
|
|
122
|
-
with tx.Horizontal(id='input-horizontal'):
|
|
123
|
-
yield tx.Static('>', id='input-glyph')
|
|
124
|
-
yield InputTextArea(placeholder='...', id='input')
|
|
55
|
+
yield InputOuter(id='input-outer')
|
|
125
56
|
|
|
126
57
|
#
|
|
127
58
|
|
|
128
59
|
def _get_input_text_area(self) -> InputTextArea:
|
|
129
60
|
return self.query_one('#input', InputTextArea)
|
|
130
61
|
|
|
62
|
+
def _get_messages_scroll(self) -> tx.VerticalScroll:
|
|
63
|
+
return self.query_one('#messages-scroll', tx.VerticalScroll)
|
|
64
|
+
|
|
131
65
|
def _get_messages_container(self) -> tx.Static:
|
|
132
66
|
return self.query_one('#messages-container', tx.Static)
|
|
133
67
|
|
|
134
68
|
#
|
|
135
69
|
|
|
70
|
+
def _is_messages_at_bottom(self, threshold: int = 3) -> bool:
|
|
71
|
+
return (ms := self._get_messages_scroll()).scroll_y >= (ms.max_scroll_y - threshold)
|
|
72
|
+
|
|
73
|
+
def _scroll_messages_to_bottom(self) -> None:
|
|
74
|
+
self._get_messages_scroll().scroll_end(animate=False)
|
|
75
|
+
|
|
76
|
+
def _anchor_messages(self) -> None:
|
|
77
|
+
if (ms := self._get_messages_scroll()).max_scroll_y:
|
|
78
|
+
ms.anchor()
|
|
79
|
+
|
|
80
|
+
#
|
|
81
|
+
|
|
136
82
|
_pending_mount_messages: list[tx.Widget] | None = None
|
|
137
83
|
|
|
138
84
|
async def _enqueue_mount_messages(self, *messages: tx.Widget) -> None:
|
|
@@ -141,16 +87,51 @@ class ChatApp(tx.App):
|
|
|
141
87
|
|
|
142
88
|
lst.extend(messages)
|
|
143
89
|
|
|
90
|
+
_stream_ai_message: StreamAiMessage | None = None
|
|
91
|
+
|
|
92
|
+
async def _finalize_stream_ai_message(self) -> None:
|
|
93
|
+
if self._stream_ai_message is None:
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
await self._stream_ai_message.stop_stream()
|
|
97
|
+
self._stream_ai_message = None
|
|
98
|
+
|
|
99
|
+
async def _append_stream_ai_message_content(self, content: str) -> None:
|
|
100
|
+
if (sam := self._stream_ai_message) is not None:
|
|
101
|
+
was_at_bottom = self._is_messages_at_bottom()
|
|
102
|
+
|
|
103
|
+
await sam.append_content(content)
|
|
104
|
+
|
|
105
|
+
self.call_after_refresh(lambda: self._get_messages_container().scroll_end(animate=False))
|
|
106
|
+
|
|
107
|
+
if was_at_bottom:
|
|
108
|
+
self.call_after_refresh(self._anchor_messages)
|
|
109
|
+
|
|
110
|
+
else:
|
|
111
|
+
await self._mount_messages(StreamAiMessage(content))
|
|
112
|
+
|
|
144
113
|
async def _mount_messages(self, *messages: tx.Widget) -> None:
|
|
114
|
+
was_at_bottom = self._is_messages_at_bottom()
|
|
115
|
+
|
|
145
116
|
msg_ctr = self._get_messages_container()
|
|
146
117
|
|
|
147
118
|
for msg in [*(self._pending_mount_messages or []), *messages]:
|
|
119
|
+
if isinstance(msg, AiMessage):
|
|
120
|
+
await self._finalize_stream_ai_message()
|
|
121
|
+
|
|
148
122
|
await msg_ctr.mount(msg)
|
|
149
123
|
|
|
124
|
+
if isinstance(msg, StreamAiMessage):
|
|
125
|
+
self._stream_ai_message = check.replacing_none(self._stream_ai_message, msg)
|
|
126
|
+
await msg.write_initial_content()
|
|
127
|
+
|
|
150
128
|
self._pending_mount_messages = None
|
|
151
129
|
|
|
152
130
|
self.call_after_refresh(lambda: msg_ctr.scroll_end(animate=False))
|
|
153
131
|
|
|
132
|
+
if was_at_bottom:
|
|
133
|
+
self.call_after_refresh(self._anchor_messages)
|
|
134
|
+
|
|
154
135
|
#
|
|
155
136
|
|
|
156
137
|
_event_queue_task: asyncio.Task[None] | None = None
|
|
@@ -167,8 +148,9 @@ class ChatApp(tx.App):
|
|
|
167
148
|
for ai_msg in ev.chat:
|
|
168
149
|
if isinstance(ai_msg, mc.AiMessage):
|
|
169
150
|
wx.append(
|
|
170
|
-
|
|
151
|
+
StaticAiMessage(
|
|
171
152
|
check.isinstance(ai_msg.c, str),
|
|
153
|
+
markdown=True,
|
|
172
154
|
),
|
|
173
155
|
)
|
|
174
156
|
|
|
@@ -176,13 +158,35 @@ class ChatApp(tx.App):
|
|
|
176
158
|
await self._enqueue_mount_messages(*wx)
|
|
177
159
|
self.call_later(self._mount_messages)
|
|
178
160
|
|
|
161
|
+
elif isinstance(ev, AiDeltaChatEvent):
|
|
162
|
+
cd = check.isinstance(ev.delta, mc.ContentAiDelta)
|
|
163
|
+
cc = check.isinstance(cd.c, str)
|
|
164
|
+
self.call_later(self. _append_stream_ai_message_content, cc)
|
|
165
|
+
|
|
166
|
+
#
|
|
167
|
+
|
|
168
|
+
# def _schedule_after_refresh(self) -> None:
|
|
169
|
+
# self.call_after_refresh(self._after_refresh)
|
|
170
|
+
|
|
171
|
+
# def _after_refresh(self) -> None:
|
|
172
|
+
# self.after_repaint()
|
|
173
|
+
#
|
|
174
|
+
# self._schedule_after_refresh()
|
|
175
|
+
|
|
176
|
+
# def after_repaint(self) -> None:
|
|
177
|
+
# # from omdev.tui.textual.debug.dominfo import inspect_dom_node # noqa
|
|
178
|
+
#
|
|
179
|
+
# pass
|
|
180
|
+
|
|
179
181
|
#
|
|
180
182
|
|
|
181
183
|
async def on_mount(self) -> None:
|
|
184
|
+
# self._schedule_after_refresh()
|
|
185
|
+
|
|
182
186
|
check.state(self._event_queue_task is None)
|
|
183
187
|
self._event_queue_task = asyncio.create_task(self._event_queue_task_main())
|
|
184
188
|
|
|
185
|
-
await self.
|
|
189
|
+
await self._chat_driver.start()
|
|
186
190
|
|
|
187
191
|
self._get_input_text_area().focus()
|
|
188
192
|
|
|
@@ -193,7 +197,7 @@ class ChatApp(tx.App):
|
|
|
193
197
|
)
|
|
194
198
|
|
|
195
199
|
async def on_unmount(self) -> None:
|
|
196
|
-
await self.
|
|
200
|
+
await self._chat_driver.stop()
|
|
197
201
|
|
|
198
202
|
if (eqt := self._event_queue_task) is not None:
|
|
199
203
|
await self._event_queue.put(None)
|
|
@@ -202,10 +206,12 @@ class ChatApp(tx.App):
|
|
|
202
206
|
async def on_input_text_area_submitted(self, event: InputTextArea.Submitted) -> None:
|
|
203
207
|
self._get_input_text_area().clear()
|
|
204
208
|
|
|
209
|
+
await self._finalize_stream_ai_message()
|
|
210
|
+
|
|
205
211
|
await self._mount_messages(
|
|
206
212
|
UserMessage(
|
|
207
213
|
event.text,
|
|
208
214
|
),
|
|
209
215
|
)
|
|
210
216
|
|
|
211
|
-
await self.
|
|
217
|
+
await self._chat_driver.send_user_messages([mc.UserMessage(event.text)])
|
|
@@ -5,18 +5,24 @@ FIXME:
|
|
|
5
5
|
import asyncio
|
|
6
6
|
|
|
7
7
|
from omlish import inject as inj
|
|
8
|
+
from omlish import lang
|
|
8
9
|
|
|
9
|
-
from ...
|
|
10
|
+
from ...drivers.events.injection import event_callbacks
|
|
10
11
|
from ..base import ChatInterface
|
|
11
|
-
from
|
|
12
|
+
from ..configs import InterfaceConfig
|
|
12
13
|
from .app import ChatApp
|
|
14
|
+
from .app import ChatDriverEventQueue
|
|
13
15
|
from .interface import TextualChatInterface
|
|
14
16
|
|
|
15
17
|
|
|
18
|
+
with lang.auto_proxy_import(globals()):
|
|
19
|
+
from ...drivers.tools import confirmation as _tools_confirmation
|
|
20
|
+
|
|
21
|
+
|
|
16
22
|
##
|
|
17
23
|
|
|
18
24
|
|
|
19
|
-
def bind_textual() -> inj.Elements:
|
|
25
|
+
def bind_textual(cfg: InterfaceConfig = InterfaceConfig()) -> inj.Elements:
|
|
20
26
|
els: list[inj.Elemental] = [
|
|
21
27
|
inj.bind(ChatInterface, to_ctor=TextualChatInterface, singleton=True),
|
|
22
28
|
]
|
|
@@ -30,14 +36,32 @@ def bind_textual() -> inj.Elements:
|
|
|
30
36
|
#
|
|
31
37
|
|
|
32
38
|
els.extend([
|
|
33
|
-
inj.bind(
|
|
39
|
+
inj.bind(ChatDriverEventQueue, to_const=asyncio.Queue()),
|
|
34
40
|
|
|
35
41
|
event_callbacks().bind_item(to_fn=inj.KwargsTarget.of(
|
|
36
42
|
lambda eq: lambda ev: eq.put(ev),
|
|
37
|
-
eq=
|
|
43
|
+
eq=ChatDriverEventQueue,
|
|
38
44
|
)),
|
|
39
45
|
])
|
|
40
46
|
|
|
41
47
|
#
|
|
42
48
|
|
|
49
|
+
if cfg.enable_tools:
|
|
50
|
+
if cfg.dangerous_no_tool_confirmation:
|
|
51
|
+
els.append(inj.bind(
|
|
52
|
+
_tools_confirmation.ToolExecutionConfirmation,
|
|
53
|
+
to_ctor=_tools_confirmation.UnsafeAlwaysAllowToolExecutionConfirmation,
|
|
54
|
+
singleton=True,
|
|
55
|
+
))
|
|
56
|
+
|
|
57
|
+
else:
|
|
58
|
+
# els.append(inj.bind(
|
|
59
|
+
# _tools_confirmation.ToolExecutionConfirmation,
|
|
60
|
+
# to_ctor=_tools.InteractiveToolExecutionConfirmation,
|
|
61
|
+
# singleton=True,
|
|
62
|
+
# ))
|
|
63
|
+
raise NotImplementedError
|
|
64
|
+
|
|
65
|
+
#
|
|
66
|
+
|
|
43
67
|
return inj.as_elements(*els)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import io as _io
|
|
2
|
+
|
|
3
|
+
from omlish import lang as _lang
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
##
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@_lang.cached_function
|
|
10
|
+
def read_app_css() -> str:
|
|
11
|
+
tcss_rsrcs = [
|
|
12
|
+
rsrc
|
|
13
|
+
for rsrc in _lang.get_relative_resources(globals=globals()).values()
|
|
14
|
+
if rsrc.name.endswith('.tcss')
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
out = _io.StringIO()
|
|
18
|
+
|
|
19
|
+
for i, rsrc in enumerate(tcss_rsrcs):
|
|
20
|
+
if i:
|
|
21
|
+
out.write('\n\n')
|
|
22
|
+
|
|
23
|
+
out.write(f'/*** {rsrc.name} ***/\n')
|
|
24
|
+
out.write('\n')
|
|
25
|
+
|
|
26
|
+
out.write(rsrc.read_text().strip())
|
|
27
|
+
out.write('\n')
|
|
28
|
+
|
|
29
|
+
return out.getvalue()
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* Container */
|
|
2
|
+
|
|
1
3
|
#messages-scroll {
|
|
2
4
|
width: 100%;
|
|
3
5
|
height: 1fr;
|
|
@@ -6,8 +8,8 @@
|
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
#messages-container {
|
|
9
|
-
height: auto;
|
|
10
11
|
width: 100%;
|
|
12
|
+
height: auto;
|
|
11
13
|
|
|
12
14
|
margin-top: 1;
|
|
13
15
|
margin-bottom: 0;
|
|
@@ -16,6 +18,9 @@
|
|
|
16
18
|
text-align: left;
|
|
17
19
|
}
|
|
18
20
|
|
|
21
|
+
|
|
22
|
+
/* Welcome */
|
|
23
|
+
|
|
19
24
|
.welcome-message {
|
|
20
25
|
margin: 1;
|
|
21
26
|
|
|
@@ -26,8 +31,74 @@
|
|
|
26
31
|
text-align: center;
|
|
27
32
|
}
|
|
28
33
|
|
|
34
|
+
|
|
35
|
+
/* User */
|
|
36
|
+
|
|
29
37
|
.user-message {
|
|
38
|
+
width: 100%;
|
|
39
|
+
height: auto;
|
|
40
|
+
|
|
41
|
+
margin-top: 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.user-message-outer {
|
|
45
|
+
width: 100%;
|
|
46
|
+
height: auto;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.user-message-glyph {
|
|
50
|
+
width: auto;
|
|
51
|
+
height: auto;
|
|
52
|
+
|
|
53
|
+
background: transparent;
|
|
54
|
+
color: $primary;
|
|
55
|
+
|
|
56
|
+
text-style: bold;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.user-message-inner {
|
|
60
|
+
width: 100%;
|
|
61
|
+
height: auto;
|
|
30
62
|
}
|
|
31
63
|
|
|
64
|
+
|
|
65
|
+
/* Ai */
|
|
66
|
+
|
|
32
67
|
.ai-message {
|
|
68
|
+
width: 100%;
|
|
69
|
+
height: auto;
|
|
70
|
+
|
|
71
|
+
margin-top: 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.ai-message-outer {
|
|
75
|
+
width: 100%;
|
|
76
|
+
height: auto;
|
|
77
|
+
|
|
78
|
+
align: left top;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.ai-message-glyph {
|
|
82
|
+
width: auto;
|
|
83
|
+
height: auto;
|
|
84
|
+
|
|
85
|
+
background: transparent;
|
|
86
|
+
color: $primary;
|
|
87
|
+
|
|
88
|
+
text-style: bold;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.ai-message-inner {
|
|
92
|
+
width: 100%;
|
|
93
|
+
height: auto;
|
|
94
|
+
|
|
95
|
+
padding: 0;
|
|
96
|
+
|
|
97
|
+
Markdown {
|
|
98
|
+
width: 100%;
|
|
99
|
+
height: auto;
|
|
100
|
+
|
|
101
|
+
margin: 0;
|
|
102
|
+
padding: 0;
|
|
103
|
+
}
|
|
33
104
|
}
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import dataclasses as dc
|
|
2
|
+
import typing as ta
|
|
3
|
+
|
|
4
|
+
from omdev.tui import textual as tx
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
##
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InputTextArea(tx.TextArea):
|
|
11
|
+
@dc.dataclass()
|
|
12
|
+
class Submitted(tx.Message):
|
|
13
|
+
text: str
|
|
14
|
+
|
|
15
|
+
def __init__(self, **kwargs: ta.Any) -> None:
|
|
16
|
+
super().__init__(**kwargs)
|
|
17
|
+
|
|
18
|
+
async def _on_key(self, event: tx.Key) -> None:
|
|
19
|
+
if event.key == 'enter':
|
|
20
|
+
event.prevent_default()
|
|
21
|
+
event.stop()
|
|
22
|
+
|
|
23
|
+
if text := self.text.strip():
|
|
24
|
+
self.post_message(self.Submitted(text))
|
|
25
|
+
|
|
26
|
+
else:
|
|
27
|
+
await super()._on_key(event)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class InputOuter(tx.Static):
|
|
31
|
+
def compose(self) -> tx.ComposeResult:
|
|
32
|
+
with tx.Vertical(id='input-vertical'):
|
|
33
|
+
with tx.Vertical(id='input-vertical2'):
|
|
34
|
+
with tx.Horizontal(id='input-horizontal'):
|
|
35
|
+
yield tx.Static('>', id='input-glyph')
|
|
36
|
+
yield InputTextArea(placeholder='...', id='input')
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import typing as ta
|
|
3
|
+
|
|
4
|
+
from omdev.tui import textual as tx
|
|
5
|
+
from omlish import lang
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
##
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Message(tx.Static, lang.Abstract):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
##
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WelcomeMessage(Message):
|
|
19
|
+
def __init__(self, content: str) -> None:
|
|
20
|
+
super().__init__(content)
|
|
21
|
+
|
|
22
|
+
self.add_class('welcome-message')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class UserMessage(Message):
|
|
29
|
+
def __init__(self, content: str) -> None:
|
|
30
|
+
super().__init__()
|
|
31
|
+
|
|
32
|
+
self.add_class('user-message')
|
|
33
|
+
|
|
34
|
+
self._content = content
|
|
35
|
+
|
|
36
|
+
def compose(self) -> tx.ComposeResult:
|
|
37
|
+
with tx.Horizontal(classes='user-message-outer'):
|
|
38
|
+
yield tx.Static('> ', classes='user-message-glyph')
|
|
39
|
+
with tx.Vertical(classes='user-message-inner'):
|
|
40
|
+
yield tx.Static(self._content)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AiMessage(Message, lang.Abstract):
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
super().__init__()
|
|
49
|
+
|
|
50
|
+
self.add_class('ai-message')
|
|
51
|
+
|
|
52
|
+
def compose(self) -> tx.ComposeResult:
|
|
53
|
+
with tx.Horizontal(classes='ai-message-outer'):
|
|
54
|
+
yield tx.Static('< ', classes='ai-message-glyph')
|
|
55
|
+
with tx.Vertical(classes='ai-message-inner'):
|
|
56
|
+
yield from self._compose_content()
|
|
57
|
+
|
|
58
|
+
@abc.abstractmethod
|
|
59
|
+
def _compose_content(self) -> ta.Generator:
|
|
60
|
+
raise NotImplementedError
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class StaticAiMessage(AiMessage):
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
content: str,
|
|
67
|
+
*,
|
|
68
|
+
markdown: bool = False,
|
|
69
|
+
) -> None:
|
|
70
|
+
super().__init__()
|
|
71
|
+
|
|
72
|
+
self._content = content
|
|
73
|
+
self._markdown = markdown
|
|
74
|
+
|
|
75
|
+
def _compose_content(self) -> ta.Generator:
|
|
76
|
+
if self._markdown:
|
|
77
|
+
yield tx.Markdown(self._content)
|
|
78
|
+
else:
|
|
79
|
+
yield tx.Static(self._content)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class StreamAiMessage(AiMessage):
|
|
83
|
+
def __init__(self, content: str) -> None:
|
|
84
|
+
super().__init__()
|
|
85
|
+
|
|
86
|
+
self._content = content
|
|
87
|
+
|
|
88
|
+
def _compose_content(self) -> ta.Generator:
|
|
89
|
+
yield tx.Markdown('')
|
|
90
|
+
|
|
91
|
+
_stream_: tx.MarkdownStream | None = None
|
|
92
|
+
|
|
93
|
+
def _stream(self) -> tx.MarkdownStream:
|
|
94
|
+
if self._stream_ is None:
|
|
95
|
+
self._stream_ = tx.Markdown.get_stream(self.query_one(tx.Markdown))
|
|
96
|
+
return self._stream_
|
|
97
|
+
|
|
98
|
+
async def write_initial_content(self) -> None:
|
|
99
|
+
if self._content:
|
|
100
|
+
await self._stream().write(self._content)
|
|
101
|
+
|
|
102
|
+
async def append_content(self, content: str) -> None:
|
|
103
|
+
if not content:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
self._content += content
|
|
107
|
+
await self._stream().write(content)
|
|
108
|
+
|
|
109
|
+
async def stop_stream(self) -> None:
|
|
110
|
+
if (stream := self._stream_) is None:
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
await stream.stop()
|
|
114
|
+
self._stream_ = None
|