ommlds 0.0.0.dev489__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.
Files changed (99) hide show
  1. ommlds/.omlish-manifests.json +3 -3
  2. ommlds/README.md +11 -0
  3. ommlds/__about__.py +1 -1
  4. ommlds/backends/ollama/_dataclasses.py +53 -23
  5. ommlds/backends/ollama/protocol.py +3 -0
  6. ommlds/cli/_dataclasses.py +835 -356
  7. ommlds/cli/main.py +80 -41
  8. ommlds/cli/rendering/types.py +6 -0
  9. ommlds/cli/sessions/chat/configs.py +4 -12
  10. ommlds/cli/sessions/chat/{chat → drivers}/ai/configs.py +3 -1
  11. ommlds/cli/sessions/chat/drivers/ai/events.py +57 -0
  12. ommlds/cli/sessions/chat/{chat → drivers}/ai/inject.py +10 -3
  13. ommlds/cli/sessions/chat/{chat → drivers}/ai/rendering.py +1 -1
  14. ommlds/cli/sessions/chat/{chat → drivers}/ai/services.py +1 -1
  15. ommlds/cli/sessions/chat/{chat → drivers}/ai/tools.py +1 -1
  16. ommlds/cli/sessions/chat/{chat → drivers}/ai/types.py +9 -0
  17. ommlds/cli/sessions/chat/drivers/configs.py +25 -0
  18. ommlds/cli/sessions/chat/drivers/driver.py +49 -0
  19. ommlds/cli/sessions/chat/drivers/events/inject.py +27 -0
  20. ommlds/cli/sessions/chat/drivers/events/injection.py +14 -0
  21. ommlds/cli/sessions/chat/drivers/events/manager.py +16 -0
  22. ommlds/cli/sessions/chat/drivers/events/types.py +38 -0
  23. ommlds/cli/sessions/chat/drivers/inject.py +69 -0
  24. ommlds/cli/sessions/chat/{chat → drivers}/state/inject.py +3 -3
  25. ommlds/cli/sessions/chat/{chat → drivers}/state/types.py +1 -1
  26. ommlds/cli/sessions/chat/{tools → drivers/tools}/configs.py +2 -2
  27. ommlds/cli/sessions/chat/drivers/tools/confirmation.py +44 -0
  28. ommlds/cli/sessions/chat/{tools → drivers/tools}/execution.py +3 -4
  29. ommlds/cli/sessions/chat/{tools → drivers/tools}/fs/inject.py +3 -3
  30. ommlds/cli/sessions/chat/{tools → drivers/tools}/inject.py +4 -12
  31. ommlds/cli/sessions/chat/{tools → drivers/tools}/injection.py +1 -1
  32. ommlds/cli/sessions/chat/{tools → drivers/tools}/rendering.py +3 -3
  33. ommlds/cli/sessions/chat/{tools → drivers/tools}/todo/inject.py +3 -3
  34. ommlds/cli/sessions/chat/{tools → drivers/tools}/weather/tools.py +1 -1
  35. ommlds/cli/sessions/chat/drivers/types.py +10 -0
  36. ommlds/cli/sessions/chat/{chat → drivers}/user/configs.py +0 -2
  37. ommlds/cli/sessions/chat/drivers/user/inject.py +40 -0
  38. ommlds/cli/sessions/chat/inject.py +4 -32
  39. ommlds/cli/sessions/chat/interfaces/bare/inject.py +61 -0
  40. ommlds/cli/sessions/chat/interfaces/bare/interactive.py +41 -0
  41. ommlds/cli/sessions/chat/{interface/bare/interface.py → interfaces/bare/oneshot.py} +5 -3
  42. ommlds/cli/sessions/chat/{tools/confirmation.py → interfaces/bare/tools.py} +3 -22
  43. ommlds/cli/sessions/chat/{interface → interfaces}/bare/user.py +1 -1
  44. ommlds/cli/sessions/chat/{interface → interfaces}/configs.py +8 -0
  45. ommlds/cli/sessions/chat/interfaces/textual/__init__.py +0 -0
  46. ommlds/cli/sessions/chat/interfaces/textual/app.py +217 -0
  47. ommlds/cli/sessions/chat/interfaces/textual/inject.py +67 -0
  48. ommlds/cli/sessions/chat/{interface → interfaces}/textual/interface.py +0 -3
  49. ommlds/cli/sessions/chat/interfaces/textual/styles/__init__.py +29 -0
  50. ommlds/cli/sessions/chat/interfaces/textual/styles/input.tcss +51 -0
  51. ommlds/cli/sessions/chat/interfaces/textual/styles/markdown.tcss +7 -0
  52. ommlds/cli/sessions/chat/interfaces/textual/styles/messages.tcss +104 -0
  53. ommlds/cli/sessions/chat/interfaces/textual/widgets/__init__.py +0 -0
  54. ommlds/cli/sessions/chat/interfaces/textual/widgets/input.py +36 -0
  55. ommlds/cli/sessions/chat/interfaces/textual/widgets/messages.py +114 -0
  56. ommlds/cli/sessions/chat/session.py +1 -1
  57. ommlds/minichain/backends/impls/ollama/chat.py +24 -56
  58. ommlds/minichain/backends/impls/ollama/protocol.py +144 -0
  59. ommlds/nanochat/rustbpe/README.md +9 -0
  60. {ommlds-0.0.0.dev489.dist-info → ommlds-0.0.0.dev491.dist-info}/METADATA +6 -6
  61. {ommlds-0.0.0.dev489.dist-info → ommlds-0.0.0.dev491.dist-info}/RECORD +91 -73
  62. ommlds/cli/sessions/chat/chat/user/inject.py +0 -52
  63. ommlds/cli/sessions/chat/chat/user/oneshot.py +0 -25
  64. ommlds/cli/sessions/chat/chat/user/types.py +0 -15
  65. ommlds/cli/sessions/chat/driver.py +0 -43
  66. ommlds/cli/sessions/chat/interface/bare/inject.py +0 -32
  67. ommlds/cli/sessions/chat/interface/textual/app.py +0 -191
  68. ommlds/cli/sessions/chat/interface/textual/inject.py +0 -27
  69. ommlds/cli/sessions/chat/interface/textual/user.py +0 -20
  70. /ommlds/cli/sessions/chat/{chat → drivers}/__init__.py +0 -0
  71. /ommlds/cli/sessions/chat/{chat → drivers}/ai/__init__.py +0 -0
  72. /ommlds/cli/sessions/chat/{chat → drivers}/ai/injection.py +0 -0
  73. /ommlds/cli/sessions/chat/{chat/state → drivers/events}/__init__.py +0 -0
  74. /ommlds/cli/sessions/chat/{chat/user → drivers/phases}/__init__.py +0 -0
  75. /ommlds/cli/sessions/chat/{phases → drivers/phases}/inject.py +0 -0
  76. /ommlds/cli/sessions/chat/{phases → drivers/phases}/injection.py +0 -0
  77. /ommlds/cli/sessions/chat/{phases → drivers/phases}/manager.py +0 -0
  78. /ommlds/cli/sessions/chat/{phases → drivers/phases}/types.py +0 -0
  79. /ommlds/cli/sessions/chat/{interface → drivers/state}/__init__.py +0 -0
  80. /ommlds/cli/sessions/chat/{chat → drivers}/state/configs.py +0 -0
  81. /ommlds/cli/sessions/chat/{chat → drivers}/state/inmemory.py +0 -0
  82. /ommlds/cli/sessions/chat/{chat → drivers}/state/storage.py +0 -0
  83. /ommlds/cli/sessions/chat/{interface/bare → drivers/tools}/__init__.py +0 -0
  84. /ommlds/cli/sessions/chat/{interface/textual → drivers/tools/fs}/__init__.py +0 -0
  85. /ommlds/cli/sessions/chat/{tools → drivers/tools}/fs/configs.py +0 -0
  86. /ommlds/cli/sessions/chat/{phases → drivers/tools/todo}/__init__.py +0 -0
  87. /ommlds/cli/sessions/chat/{tools → drivers/tools}/todo/configs.py +0 -0
  88. /ommlds/cli/sessions/chat/{tools → drivers/tools/weather}/__init__.py +0 -0
  89. /ommlds/cli/sessions/chat/{tools → drivers/tools}/weather/configs.py +0 -0
  90. /ommlds/cli/sessions/chat/{tools → drivers/tools}/weather/inject.py +0 -0
  91. /ommlds/cli/sessions/chat/{tools/fs → drivers/user}/__init__.py +0 -0
  92. /ommlds/cli/sessions/chat/{tools/todo → interfaces}/__init__.py +0 -0
  93. /ommlds/cli/sessions/chat/{tools/weather → interfaces/bare}/__init__.py +0 -0
  94. /ommlds/cli/sessions/chat/{interface → interfaces}/base.py +0 -0
  95. /ommlds/cli/sessions/chat/{interface → interfaces}/inject.py +0 -0
  96. {ommlds-0.0.0.dev489.dist-info → ommlds-0.0.0.dev491.dist-info}/WHEEL +0 -0
  97. {ommlds-0.0.0.dev489.dist-info → ommlds-0.0.0.dev491.dist-info}/entry_points.txt +0 -0
  98. {ommlds-0.0.0.dev489.dist-info → ommlds-0.0.0.dev491.dist-info}/licenses/LICENSE +0 -0
  99. {ommlds-0.0.0.dev489.dist-info → ommlds-0.0.0.dev491.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,217 @@
1
+ import asyncio
2
+ import typing as ta
3
+
4
+ from omdev.tui import textual as tx
5
+ from omlish import check
6
+
7
+ from ...... import minichain as mc
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
19
+
20
+
21
+ ##
22
+
23
+
24
+ ChatDriverEventQueue = ta.NewType('ChatDriverEventQueue', asyncio.Queue)
25
+
26
+
27
+ ##
28
+
29
+
30
+ class ChatApp(tx.App):
31
+ ENABLE_COMMAND_PALETTE: ta.ClassVar[bool] = False
32
+
33
+ def __init__(
34
+ self,
35
+ *,
36
+ driver: ChatDriver,
37
+ event_queue: ChatDriverEventQueue,
38
+ ) -> None:
39
+ super().__init__()
40
+
41
+ self._chat_driver = driver
42
+ self._event_queue = event_queue
43
+
44
+ def get_driver_class(self) -> type[tx.Driver]:
45
+ return tx.get_pending_writes_driver_class(super().get_driver_class())
46
+
47
+ CSS: ta.ClassVar[str] = read_app_css()
48
+
49
+ #
50
+
51
+ def compose(self) -> tx.ComposeResult:
52
+ with tx.VerticalScroll(id='messages-scroll'):
53
+ yield tx.Static(id='messages-container')
54
+
55
+ yield InputOuter(id='input-outer')
56
+
57
+ #
58
+
59
+ def _get_input_text_area(self) -> InputTextArea:
60
+ return self.query_one('#input', InputTextArea)
61
+
62
+ def _get_messages_scroll(self) -> tx.VerticalScroll:
63
+ return self.query_one('#messages-scroll', tx.VerticalScroll)
64
+
65
+ def _get_messages_container(self) -> tx.Static:
66
+ return self.query_one('#messages-container', tx.Static)
67
+
68
+ #
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
+
82
+ _pending_mount_messages: list[tx.Widget] | None = None
83
+
84
+ async def _enqueue_mount_messages(self, *messages: tx.Widget) -> None:
85
+ if (lst := self._pending_mount_messages) is None:
86
+ lst = self._pending_mount_messages = []
87
+
88
+ lst.extend(messages)
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
+
113
+ async def _mount_messages(self, *messages: tx.Widget) -> None:
114
+ was_at_bottom = self._is_messages_at_bottom()
115
+
116
+ msg_ctr = self._get_messages_container()
117
+
118
+ for msg in [*(self._pending_mount_messages or []), *messages]:
119
+ if isinstance(msg, AiMessage):
120
+ await self._finalize_stream_ai_message()
121
+
122
+ await msg_ctr.mount(msg)
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
+
128
+ self._pending_mount_messages = None
129
+
130
+ self.call_after_refresh(lambda: msg_ctr.scroll_end(animate=False))
131
+
132
+ if was_at_bottom:
133
+ self.call_after_refresh(self._anchor_messages)
134
+
135
+ #
136
+
137
+ _event_queue_task: asyncio.Task[None] | None = None
138
+
139
+ async def _event_queue_task_main(self) -> None:
140
+ while True:
141
+ ev = await self._event_queue.get()
142
+ if ev is None:
143
+ break
144
+
145
+ if isinstance(ev, AiMessagesChatEvent):
146
+ wx: list[tx.Widget] = []
147
+
148
+ for ai_msg in ev.chat:
149
+ if isinstance(ai_msg, mc.AiMessage):
150
+ wx.append(
151
+ StaticAiMessage(
152
+ check.isinstance(ai_msg.c, str),
153
+ markdown=True,
154
+ ),
155
+ )
156
+
157
+ if wx:
158
+ await self._enqueue_mount_messages(*wx)
159
+ self.call_later(self._mount_messages)
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
+
181
+ #
182
+
183
+ async def on_mount(self) -> None:
184
+ # self._schedule_after_refresh()
185
+
186
+ check.state(self._event_queue_task is None)
187
+ self._event_queue_task = asyncio.create_task(self._event_queue_task_main())
188
+
189
+ await self._chat_driver.start()
190
+
191
+ self._get_input_text_area().focus()
192
+
193
+ await self._mount_messages(
194
+ WelcomeMessage(
195
+ 'Hello!',
196
+ ),
197
+ )
198
+
199
+ async def on_unmount(self) -> None:
200
+ await self._chat_driver.stop()
201
+
202
+ if (eqt := self._event_queue_task) is not None:
203
+ await self._event_queue.put(None)
204
+ await eqt
205
+
206
+ async def on_input_text_area_submitted(self, event: InputTextArea.Submitted) -> None:
207
+ self._get_input_text_area().clear()
208
+
209
+ await self._finalize_stream_ai_message()
210
+
211
+ await self._mount_messages(
212
+ UserMessage(
213
+ event.text,
214
+ ),
215
+ )
216
+
217
+ await self._chat_driver.send_user_messages([mc.UserMessage(event.text)])
@@ -0,0 +1,67 @@
1
+ """
2
+ FIXME:
3
+ - too lazy to lazy import guts like every other proper inject module lol >_<
4
+ """
5
+ import asyncio
6
+
7
+ from omlish import inject as inj
8
+ from omlish import lang
9
+
10
+ from ...drivers.events.injection import event_callbacks
11
+ from ..base import ChatInterface
12
+ from ..configs import InterfaceConfig
13
+ from .app import ChatApp
14
+ from .app import ChatDriverEventQueue
15
+ from .interface import TextualChatInterface
16
+
17
+
18
+ with lang.auto_proxy_import(globals()):
19
+ from ...drivers.tools import confirmation as _tools_confirmation
20
+
21
+
22
+ ##
23
+
24
+
25
+ def bind_textual(cfg: InterfaceConfig = InterfaceConfig()) -> inj.Elements:
26
+ els: list[inj.Elemental] = [
27
+ inj.bind(ChatInterface, to_ctor=TextualChatInterface, singleton=True),
28
+ ]
29
+
30
+ #
31
+
32
+ els.extend([
33
+ inj.bind(ChatApp, singleton=True),
34
+ ])
35
+
36
+ #
37
+
38
+ els.extend([
39
+ inj.bind(ChatDriverEventQueue, to_const=asyncio.Queue()),
40
+
41
+ event_callbacks().bind_item(to_fn=inj.KwargsTarget.of(
42
+ lambda eq: lambda ev: eq.put(ev),
43
+ eq=ChatDriverEventQueue,
44
+ )),
45
+ ])
46
+
47
+ #
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
+
67
+ return inj.as_elements(*els)
@@ -1,4 +1,3 @@
1
- from ...driver import ChatDriver
2
1
  from ..base import ChatInterface
3
2
  from .app import ChatApp
4
3
 
@@ -10,12 +9,10 @@ class TextualChatInterface(ChatInterface):
10
9
  def __init__(
11
10
  self,
12
11
  *,
13
- driver: ChatDriver,
14
12
  app: ChatApp,
15
13
  ) -> None:
16
14
  super().__init__()
17
15
 
18
- self._driver = driver
19
16
  self._app = app
20
17
 
21
18
  async def run(self) -> None:
@@ -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()
@@ -0,0 +1,51 @@
1
+ #input-outer {
2
+ width: 100%;
3
+ height: auto;
4
+ }
5
+
6
+ #input-vertical {
7
+ width: 100%;
8
+ height: auto;
9
+
10
+ margin: 0 2 1 2;
11
+
12
+ padding: 0;
13
+ }
14
+
15
+ #input-vertical2 {
16
+ width: 100%;
17
+ height: auto;
18
+
19
+ border: round $foreground-muted;
20
+
21
+ padding: 0 1;
22
+ }
23
+
24
+ #input-horizontal {
25
+ width: 100%;
26
+ height: auto;
27
+ }
28
+
29
+ #input-glyph {
30
+ width: auto;
31
+
32
+ padding: 0 1 0 0;
33
+
34
+ background: transparent;
35
+ color: $primary;
36
+
37
+ text-style: bold;
38
+ }
39
+
40
+ #input {
41
+ width: 1fr;
42
+ height: auto;
43
+ max-height: 16;
44
+
45
+ border: none;
46
+
47
+ padding: 0;
48
+
49
+ background: transparent;
50
+ color: $text;
51
+ }
@@ -0,0 +1,7 @@
1
+ Markdown MarkdownFence {
2
+ max-width: 95%;
3
+
4
+ overflow-x: auto;
5
+
6
+ scrollbar-size-horizontal: 1;
7
+ }
@@ -0,0 +1,104 @@
1
+ /* Container */
2
+
3
+ #messages-scroll {
4
+ width: 100%;
5
+ height: 1fr;
6
+
7
+ padding: 0 2 0 2;
8
+ }
9
+
10
+ #messages-container {
11
+ width: 100%;
12
+ height: auto;
13
+
14
+ margin-top: 1;
15
+ margin-bottom: 0;
16
+
17
+ layout: stream;
18
+ text-align: left;
19
+ }
20
+
21
+
22
+ /* Welcome */
23
+
24
+ .welcome-message {
25
+ margin: 1;
26
+
27
+ border: round;
28
+
29
+ padding: 1;
30
+
31
+ text-align: center;
32
+ }
33
+
34
+
35
+ /* User */
36
+
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;
62
+ }
63
+
64
+
65
+ /* Ai */
66
+
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
+ }
104
+ }
@@ -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
@@ -4,7 +4,7 @@ from omlish import dataclasses as dc
4
4
 
5
5
  from ..base import Session
6
6
  from .configs import ChatConfig
7
- from .interface.base import ChatInterface
7
+ from .interfaces.base import ChatInterface
8
8
 
9
9
 
10
10
  ##