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.
Files changed (83) 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 +439 -289
  7. ommlds/cli/main.py +40 -33
  8. ommlds/cli/rendering/types.py +6 -0
  9. ommlds/cli/sessions/chat/configs.py +2 -2
  10. ommlds/cli/sessions/chat/{agents → drivers}/ai/inject.py +3 -1
  11. ommlds/cli/sessions/chat/{agents → drivers}/configs.py +1 -1
  12. ommlds/cli/sessions/chat/{agents/agent.py → drivers/driver.py} +1 -1
  13. ommlds/cli/sessions/chat/{agents → drivers}/inject.py +13 -6
  14. ommlds/cli/sessions/chat/{agents → drivers}/tools/configs.py +0 -2
  15. ommlds/cli/sessions/chat/drivers/tools/confirmation.py +44 -0
  16. ommlds/cli/sessions/chat/{agents → drivers}/tools/execution.py +2 -3
  17. ommlds/cli/sessions/chat/{agents → drivers}/tools/inject.py +1 -13
  18. ommlds/cli/sessions/chat/{agents → drivers}/tools/rendering.py +1 -1
  19. ommlds/cli/sessions/chat/drivers/types.py +10 -0
  20. ommlds/cli/sessions/chat/{agents → drivers}/user/inject.py +3 -3
  21. ommlds/cli/sessions/chat/inject.py +2 -2
  22. ommlds/cli/sessions/chat/interfaces/bare/inject.py +23 -0
  23. ommlds/cli/sessions/chat/interfaces/bare/interactive.py +6 -6
  24. ommlds/cli/sessions/chat/interfaces/bare/oneshot.py +5 -5
  25. ommlds/cli/sessions/chat/{agents/tools/confirmation.py → interfaces/bare/tools.py} +2 -21
  26. ommlds/cli/sessions/chat/interfaces/bare/user.py +1 -1
  27. ommlds/cli/sessions/chat/interfaces/configs.py +8 -0
  28. ommlds/cli/sessions/chat/interfaces/textual/app.py +100 -94
  29. ommlds/cli/sessions/chat/interfaces/textual/inject.py +29 -5
  30. ommlds/cli/sessions/chat/interfaces/textual/styles/__init__.py +29 -0
  31. ommlds/cli/sessions/chat/interfaces/textual/styles/markdown.tcss +7 -0
  32. ommlds/cli/sessions/chat/interfaces/textual/styles/messages.tcss +72 -1
  33. ommlds/cli/sessions/chat/interfaces/textual/widgets/__init__.py +0 -0
  34. ommlds/cli/sessions/chat/interfaces/textual/widgets/input.py +36 -0
  35. ommlds/cli/sessions/chat/interfaces/textual/widgets/messages.py +114 -0
  36. ommlds/minichain/backends/impls/ollama/chat.py +24 -56
  37. ommlds/minichain/backends/impls/ollama/protocol.py +144 -0
  38. ommlds/nanochat/rustbpe/README.md +9 -0
  39. {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev491.dist-info}/METADATA +6 -6
  40. {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev491.dist-info}/RECORD +83 -74
  41. /ommlds/cli/sessions/chat/{agents → drivers}/__init__.py +0 -0
  42. /ommlds/cli/sessions/chat/{agents → drivers}/ai/__init__.py +0 -0
  43. /ommlds/cli/sessions/chat/{agents → drivers}/ai/configs.py +0 -0
  44. /ommlds/cli/sessions/chat/{agents → drivers}/ai/events.py +0 -0
  45. /ommlds/cli/sessions/chat/{agents → drivers}/ai/injection.py +0 -0
  46. /ommlds/cli/sessions/chat/{agents → drivers}/ai/rendering.py +0 -0
  47. /ommlds/cli/sessions/chat/{agents → drivers}/ai/services.py +0 -0
  48. /ommlds/cli/sessions/chat/{agents → drivers}/ai/tools.py +0 -0
  49. /ommlds/cli/sessions/chat/{agents → drivers}/ai/types.py +0 -0
  50. /ommlds/cli/sessions/chat/{agents → drivers}/events/__init__.py +0 -0
  51. /ommlds/cli/sessions/chat/{agents → drivers}/events/inject.py +0 -0
  52. /ommlds/cli/sessions/chat/{agents → drivers}/events/injection.py +0 -0
  53. /ommlds/cli/sessions/chat/{agents → drivers}/events/manager.py +0 -0
  54. /ommlds/cli/sessions/chat/{agents → drivers}/events/types.py +0 -0
  55. /ommlds/cli/sessions/chat/{agents → drivers}/phases/__init__.py +0 -0
  56. /ommlds/cli/sessions/chat/{agents → drivers}/phases/inject.py +0 -0
  57. /ommlds/cli/sessions/chat/{agents → drivers}/phases/injection.py +0 -0
  58. /ommlds/cli/sessions/chat/{agents → drivers}/phases/manager.py +0 -0
  59. /ommlds/cli/sessions/chat/{agents → drivers}/phases/types.py +0 -0
  60. /ommlds/cli/sessions/chat/{agents → drivers}/state/__init__.py +0 -0
  61. /ommlds/cli/sessions/chat/{agents → drivers}/state/configs.py +0 -0
  62. /ommlds/cli/sessions/chat/{agents → drivers}/state/inject.py +0 -0
  63. /ommlds/cli/sessions/chat/{agents → drivers}/state/inmemory.py +0 -0
  64. /ommlds/cli/sessions/chat/{agents → drivers}/state/storage.py +0 -0
  65. /ommlds/cli/sessions/chat/{agents → drivers}/state/types.py +0 -0
  66. /ommlds/cli/sessions/chat/{agents → drivers}/tools/__init__.py +0 -0
  67. /ommlds/cli/sessions/chat/{agents → drivers}/tools/fs/__init__.py +0 -0
  68. /ommlds/cli/sessions/chat/{agents → drivers}/tools/fs/configs.py +0 -0
  69. /ommlds/cli/sessions/chat/{agents → drivers}/tools/fs/inject.py +0 -0
  70. /ommlds/cli/sessions/chat/{agents → drivers}/tools/injection.py +0 -0
  71. /ommlds/cli/sessions/chat/{agents → drivers}/tools/todo/__init__.py +0 -0
  72. /ommlds/cli/sessions/chat/{agents → drivers}/tools/todo/configs.py +0 -0
  73. /ommlds/cli/sessions/chat/{agents → drivers}/tools/todo/inject.py +0 -0
  74. /ommlds/cli/sessions/chat/{agents → drivers}/tools/weather/__init__.py +0 -0
  75. /ommlds/cli/sessions/chat/{agents → drivers}/tools/weather/configs.py +0 -0
  76. /ommlds/cli/sessions/chat/{agents → drivers}/tools/weather/inject.py +0 -0
  77. /ommlds/cli/sessions/chat/{agents → drivers}/tools/weather/tools.py +0 -0
  78. /ommlds/cli/sessions/chat/{agents → drivers}/user/__init__.py +0 -0
  79. /ommlds/cli/sessions/chat/{agents → drivers}/user/configs.py +0 -0
  80. {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev491.dist-info}/WHEEL +0 -0
  81. {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev491.dist-info}/entry_points.txt +0 -0
  82. {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev491.dist-info}/licenses/LICENSE +0 -0
  83. {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 ...agents.agent import ChatAgent
12
- from ...agents.events.types import AiMessagesChatEvent
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
- ChatAgentEventQueue = ta.NewType('ChatAgentEventQueue', asyncio.Queue)
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
- agent: ChatAgent,
102
- event_queue: ChatAgentEventQueue,
36
+ driver: ChatDriver,
37
+ event_queue: ChatDriverEventQueue,
103
38
  ) -> None:
104
39
  super().__init__()
105
40
 
106
- self._agent = agent
41
+ self._chat_driver = driver
107
42
  self._event_queue = event_queue
108
43
 
109
- CSS: ta.ClassVar[str] = _read_app_css()
44
+ def get_driver_class(self) -> type[tx.Driver]:
45
+ return tx.get_pending_writes_driver_class(super().get_driver_class())
110
46
 
111
- ENABLE_COMMAND_PALETTE: ta.ClassVar[bool] = False
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
- with tx.Static(id='input-outer'):
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
- AiMessage(
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._agent.start()
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._agent.stop()
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._agent.send_user_messages([mc.UserMessage(event.text)])
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 ...agents.events.injection import event_callbacks
10
+ from ...drivers.events.injection import event_callbacks
10
11
  from ..base import ChatInterface
11
- from .app import ChatAgentEventQueue
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(ChatAgentEventQueue, to_const=asyncio.Queue()),
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=ChatAgentEventQueue,
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()
@@ -0,0 +1,7 @@
1
+ Markdown MarkdownFence {
2
+ max-width: 95%;
3
+
4
+ overflow-x: auto;
5
+
6
+ scrollbar-size-horizontal: 1;
7
+ }
@@ -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
  }
@@ -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