ommlds 0.0.0.dev490__py3-none-any.whl → 0.0.0.dev492__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 +9 -7
- 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 +42 -34
- 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} +7 -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 +5 -5
- 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 +12 -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/inject.py +1 -1
- ommlds/cli/sessions/chat/interfaces/textual/app.py +154 -103
- ommlds/cli/sessions/chat/interfaces/textual/inject.py +34 -9
- ommlds/cli/sessions/chat/interfaces/textual/interface.py +85 -0
- ommlds/cli/sessions/chat/interfaces/textual/styles/__init__.py +29 -0
- ommlds/cli/sessions/chat/interfaces/textual/styles/input.tcss +3 -1
- ommlds/cli/sessions/chat/interfaces/textual/styles/markdown.tcss +7 -0
- ommlds/cli/sessions/chat/interfaces/textual/styles/messages.tcss +131 -9
- ommlds/cli/sessions/chat/interfaces/textual/tools.py +37 -0
- 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 +164 -0
- ommlds/minichain/backends/impls/ollama/chat.py +50 -56
- ommlds/minichain/backends/impls/ollama/protocol.py +144 -0
- ommlds/minichain/backends/impls/openai/names.py +3 -1
- ommlds/nanochat/rustbpe/README.md +9 -0
- {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev492.dist-info}/METADATA +6 -6
- {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev492.dist-info}/RECORD +88 -78
- /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.dev492.dist-info}/WHEEL +0 -0
- {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev492.dist-info}/entry_points.txt +0 -0
- {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev492.dist-info}/licenses/LICENSE +0 -0
- {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev492.dist-info}/top_level.txt +0 -0
|
@@ -1,33 +1,155 @@
|
|
|
1
|
-
|
|
1
|
+
/* Container */
|
|
2
|
+
|
|
3
|
+
#messages-container {
|
|
2
4
|
width: 100%;
|
|
3
5
|
height: 1fr;
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
scrollbar-gutter: stable;
|
|
8
|
+
|
|
9
|
+
background: $background-darken-3;
|
|
10
|
+
|
|
11
|
+
text-align: left;
|
|
12
|
+
|
|
13
|
+
scrollbar-size: 1 1;
|
|
6
14
|
}
|
|
7
15
|
|
|
8
|
-
|
|
16
|
+
|
|
17
|
+
/* Base */
|
|
18
|
+
|
|
19
|
+
.message {
|
|
20
|
+
width: 1fr;
|
|
9
21
|
height: auto;
|
|
10
|
-
width: 100%;
|
|
11
22
|
|
|
12
|
-
margin
|
|
13
|
-
|
|
23
|
+
margin: 1 0 0 0;
|
|
24
|
+
|
|
25
|
+
padding-right: 1;
|
|
14
26
|
|
|
15
27
|
layout: stream;
|
|
16
|
-
text-align: left;
|
|
17
28
|
}
|
|
18
29
|
|
|
30
|
+
|
|
31
|
+
/* Welcome */
|
|
32
|
+
|
|
19
33
|
.welcome-message {
|
|
20
|
-
|
|
34
|
+
padding: 1 2 1 1;
|
|
21
35
|
|
|
22
36
|
border: round;
|
|
37
|
+
}
|
|
23
38
|
|
|
24
|
-
|
|
39
|
+
.welcome-message-outer {
|
|
40
|
+
width: 1fr;
|
|
41
|
+
height: auto;
|
|
42
|
+
}
|
|
25
43
|
|
|
44
|
+
.welcome-message-content {
|
|
26
45
|
text-align: center;
|
|
27
46
|
}
|
|
28
47
|
|
|
48
|
+
|
|
49
|
+
/* User */
|
|
50
|
+
|
|
29
51
|
.user-message {
|
|
30
52
|
}
|
|
31
53
|
|
|
54
|
+
.user-message-outer {
|
|
55
|
+
width: 1fr;
|
|
56
|
+
height: auto;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.user-message-glyph {
|
|
60
|
+
width: auto;
|
|
61
|
+
height: auto;
|
|
62
|
+
|
|
63
|
+
background: transparent;
|
|
64
|
+
color: $primary;
|
|
65
|
+
|
|
66
|
+
text-style: bold;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.user-message-inner {
|
|
70
|
+
width: 1fr;
|
|
71
|
+
height: auto;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
/* Ai */
|
|
76
|
+
|
|
32
77
|
.ai-message {
|
|
33
78
|
}
|
|
79
|
+
|
|
80
|
+
.ai-message-outer {
|
|
81
|
+
width: 1fr;
|
|
82
|
+
height: auto;
|
|
83
|
+
|
|
84
|
+
align: left top;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.ai-message-glyph {
|
|
88
|
+
width: auto;
|
|
89
|
+
height: auto;
|
|
90
|
+
|
|
91
|
+
background: transparent;
|
|
92
|
+
color: $primary;
|
|
93
|
+
|
|
94
|
+
text-style: bold;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.ai-message-inner {
|
|
98
|
+
width: 1fr;
|
|
99
|
+
height: auto;
|
|
100
|
+
|
|
101
|
+
padding: 0;
|
|
102
|
+
|
|
103
|
+
Markdown {
|
|
104
|
+
width: 100%;
|
|
105
|
+
height: auto;
|
|
106
|
+
|
|
107
|
+
margin: 0;
|
|
108
|
+
padding: 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
/* Tool Confirmation */
|
|
114
|
+
|
|
115
|
+
.tool-confirmation-message {
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.tool-confirmation-message-outer {
|
|
119
|
+
width: 1fr;
|
|
120
|
+
height: auto;
|
|
121
|
+
|
|
122
|
+
align: left top;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.tool-confirmation-message-glyph {
|
|
126
|
+
width: auto;
|
|
127
|
+
height: auto;
|
|
128
|
+
|
|
129
|
+
background: transparent;
|
|
130
|
+
color: $primary;
|
|
131
|
+
|
|
132
|
+
text-style: bold;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.tool-confirmation-message-inner {
|
|
136
|
+
width: 1fr;
|
|
137
|
+
height: auto;
|
|
138
|
+
|
|
139
|
+
padding: 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.tool-confirmation-message-inner-open {
|
|
143
|
+
background: $warning-lighten-3 15%;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.tool-confirmation-message-inner-closed {
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.tool-confirmation-message-content {
|
|
150
|
+
background: $background;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.tool-confirmation-message-controls {
|
|
154
|
+
margin-top: 1;
|
|
155
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from omlish.formats import json
|
|
2
|
+
|
|
3
|
+
from ...... import minichain as mc
|
|
4
|
+
from ...drivers.tools.confirmation import ToolExecutionConfirmation
|
|
5
|
+
from ...drivers.tools.confirmation import ToolExecutionRequestDeniedError
|
|
6
|
+
from .app import ChatAppGetter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
##
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ChatAppToolExecutionConfirmation(ToolExecutionConfirmation):
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
*,
|
|
16
|
+
app: ChatAppGetter,
|
|
17
|
+
) -> None:
|
|
18
|
+
super().__init__()
|
|
19
|
+
|
|
20
|
+
self._app = app
|
|
21
|
+
|
|
22
|
+
async def confirm_tool_execution_or_raise(
|
|
23
|
+
self,
|
|
24
|
+
use: 'mc.ToolUse',
|
|
25
|
+
entry: 'mc.ToolCatalogEntry',
|
|
26
|
+
) -> None:
|
|
27
|
+
tr_dct = dict(
|
|
28
|
+
id=use.id,
|
|
29
|
+
name=entry.spec.name,
|
|
30
|
+
args=use.args,
|
|
31
|
+
# spec=msh.marshal(tce.spec),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
msg = f'Execute requested tool?\n\n{json.dumps_pretty(tr_dct)}' # noqa
|
|
35
|
+
|
|
36
|
+
if not await self._app().confirm_tool_use(msg):
|
|
37
|
+
raise ToolExecutionRequestDeniedError
|
|
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,164 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import asyncio
|
|
3
|
+
import typing as ta
|
|
4
|
+
|
|
5
|
+
from omdev.tui import textual as tx
|
|
6
|
+
from omlish import lang
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
##
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Message(tx.Static, lang.Abstract):
|
|
13
|
+
def __init__(self, *args: ta.Any, **kwargs: ta.Any) -> None:
|
|
14
|
+
super().__init__(*args, **kwargs)
|
|
15
|
+
|
|
16
|
+
self.add_class('message')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WelcomeMessage(Message):
|
|
23
|
+
def __init__(self, content: str) -> None:
|
|
24
|
+
super().__init__()
|
|
25
|
+
|
|
26
|
+
self.add_class('welcome-message')
|
|
27
|
+
|
|
28
|
+
self._content = content
|
|
29
|
+
|
|
30
|
+
def compose(self) -> tx.ComposeResult:
|
|
31
|
+
with tx.Vertical(classes='welcome-message-outer'):
|
|
32
|
+
yield tx.Static(self._content, classes='welcome-message-content')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
##
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class UserMessage(Message):
|
|
39
|
+
def __init__(self, content: str) -> None:
|
|
40
|
+
super().__init__()
|
|
41
|
+
|
|
42
|
+
self.add_class('user-message')
|
|
43
|
+
|
|
44
|
+
self._content = content
|
|
45
|
+
|
|
46
|
+
def compose(self) -> tx.ComposeResult:
|
|
47
|
+
with tx.Horizontal(classes='user-message-outer'):
|
|
48
|
+
yield tx.Static('> ', classes='user-message-glyph')
|
|
49
|
+
with tx.Vertical(classes='user-message-inner'):
|
|
50
|
+
yield tx.Static(self._content)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
##
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class AiMessage(Message, lang.Abstract):
|
|
57
|
+
def __init__(self) -> None:
|
|
58
|
+
super().__init__()
|
|
59
|
+
|
|
60
|
+
self.add_class('ai-message')
|
|
61
|
+
|
|
62
|
+
def compose(self) -> tx.ComposeResult:
|
|
63
|
+
with tx.Horizontal(classes='ai-message-outer'):
|
|
64
|
+
yield tx.Static('< ', classes='ai-message-glyph')
|
|
65
|
+
with tx.Vertical(classes='ai-message-inner'):
|
|
66
|
+
yield from self._compose_content()
|
|
67
|
+
|
|
68
|
+
@abc.abstractmethod
|
|
69
|
+
def _compose_content(self) -> ta.Generator:
|
|
70
|
+
raise NotImplementedError
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class StaticAiMessage(AiMessage):
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
content: str,
|
|
77
|
+
*,
|
|
78
|
+
markdown: bool = False,
|
|
79
|
+
) -> None:
|
|
80
|
+
super().__init__()
|
|
81
|
+
|
|
82
|
+
self._content = content
|
|
83
|
+
self._markdown = markdown
|
|
84
|
+
|
|
85
|
+
def _compose_content(self) -> ta.Generator:
|
|
86
|
+
if self._markdown:
|
|
87
|
+
yield tx.Markdown(self._content)
|
|
88
|
+
else:
|
|
89
|
+
yield tx.Static(self._content)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class StreamAiMessage(AiMessage):
|
|
93
|
+
def __init__(self, content: str) -> None:
|
|
94
|
+
super().__init__()
|
|
95
|
+
|
|
96
|
+
self._content = content
|
|
97
|
+
|
|
98
|
+
def _compose_content(self) -> ta.Generator:
|
|
99
|
+
yield tx.Markdown('')
|
|
100
|
+
|
|
101
|
+
_stream_: tx.MarkdownStream | None = None
|
|
102
|
+
|
|
103
|
+
def _stream(self) -> tx.MarkdownStream:
|
|
104
|
+
if self._stream_ is None:
|
|
105
|
+
self._stream_ = tx.Markdown.get_stream(self.query_one(tx.Markdown))
|
|
106
|
+
return self._stream_
|
|
107
|
+
|
|
108
|
+
async def write_initial_content(self) -> None:
|
|
109
|
+
if self._content:
|
|
110
|
+
await self._stream().write(self._content)
|
|
111
|
+
|
|
112
|
+
async def append_content(self, content: str) -> None:
|
|
113
|
+
if not content:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
self._content += content
|
|
117
|
+
await self._stream().write(content)
|
|
118
|
+
|
|
119
|
+
async def stop_stream(self) -> None:
|
|
120
|
+
if (stream := self._stream_) is None:
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
await stream.stop()
|
|
124
|
+
self._stream_ = None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
##
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class ToolConfirmationControls(tx.Static):
|
|
131
|
+
class Allowed(tx.Message):
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
def compose(self) -> tx.ComposeResult:
|
|
135
|
+
yield tx.Button('Allow', action='allow')
|
|
136
|
+
|
|
137
|
+
def action_allow(self) -> None:
|
|
138
|
+
self.post_message(self.Allowed())
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class ToolConfirmationMessage(Message):
|
|
142
|
+
def __init__(self, content: str, fut: asyncio.Future[bool]) -> None:
|
|
143
|
+
super().__init__()
|
|
144
|
+
|
|
145
|
+
self.add_class('tool-confirmation-message')
|
|
146
|
+
|
|
147
|
+
self._content = content
|
|
148
|
+
self._fut = fut
|
|
149
|
+
|
|
150
|
+
def compose(self) -> tx.ComposeResult:
|
|
151
|
+
with tx.Horizontal(classes='tool-confirmation-message-outer'):
|
|
152
|
+
yield tx.Static('? ', classes='tool-confirmation-message-glyph')
|
|
153
|
+
with tx.Vertical(classes='tool-confirmation-message-inner tool-confirmation-message-inner-open'):
|
|
154
|
+
yield tx.Static(self._content, classes='tool-confirmation-message-content')
|
|
155
|
+
yield ToolConfirmationControls(classes='tool-confirmation-message-controls')
|
|
156
|
+
|
|
157
|
+
@tx.on(ToolConfirmationControls.Allowed)
|
|
158
|
+
async def on_allowed(self, event: ToolConfirmationControls.Allowed) -> None:
|
|
159
|
+
inner = self.query_one(tx.Vertical)
|
|
160
|
+
await inner.query_one(ToolConfirmationControls).remove()
|
|
161
|
+
inner.remove_class('tool-confirmation-message-inner-open')
|
|
162
|
+
inner.add_class('tool-confirmation-message-inner-closed')
|
|
163
|
+
|
|
164
|
+
self._fut.set_result(True)
|
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
devstral-small-2:24b 24277f07f62d 15 GB 15 hours ago
|
|
3
|
+
|
|
4
|
+
dolphin3:latest d5ab9ae8e1f2 4.9 GB 11 months ago (no tools)
|
|
5
|
+
|
|
6
|
+
gemma3:27b a418f5838eaf 17 GB 7 weeks ago (no tools)
|
|
7
|
+
gemma3:4b a2af6cc3eb7f 3.3 GB 7 weeks ago (no tools)
|
|
8
|
+
|
|
9
|
+
llama3.2:1b baf6a787fdff 1.3 GB 13 months ago (too stupid for tools)
|
|
10
|
+
llama3.2:latest a80c4f17acd5 2.0 GB 13 months ago
|
|
11
|
+
|
|
12
|
+
ministral-3:14b 4760c35aeb9d 9.1 GB 11 hours ago
|
|
13
|
+
mistral:latest 6577803aa9a0 4.4 GB 3 seconds ago
|
|
14
|
+
|
|
15
|
+
nemotron-3-nano:30b b725f1117407 24 GB 15 hours ago
|
|
16
|
+
|
|
17
|
+
olmo-3.1:32b-instruct a16b6a5be6cf 19 GB 11 hours ago (no tools)
|
|
18
|
+
olmo-3.1:32b-think 223d4ec84d91 19 GB 11 hours ago (no tools)
|
|
19
|
+
|
|
20
|
+
phi4-mini:latest 78fad5d182a7 2.5 GB 8 months ago (no tools)
|
|
21
|
+
|
|
22
|
+
qwen3-coder:30b 06c1097efce0 18 GB 11 hours ago
|
|
23
|
+
qwen3-next:80b b2ebb986e4e9 50 GB 11 hours ago
|
|
24
|
+
qwen3:30b ad815644918f 18 GB 11 hours ago
|
|
25
|
+
qwen3:32b 030ee887880f 20 GB 11 hours ago
|
|
26
|
+
"""
|
|
1
27
|
import typing as ta
|
|
2
28
|
|
|
3
29
|
from omlish import check
|
|
@@ -16,20 +42,17 @@ from ....chat.choices.services import static_check_is_chat_choices_service
|
|
|
16
42
|
from ....chat.choices.stream.services import ChatChoicesStreamRequest
|
|
17
43
|
from ....chat.choices.stream.services import ChatChoicesStreamResponse
|
|
18
44
|
from ....chat.choices.stream.services import static_check_is_chat_choices_stream_service
|
|
19
|
-
from ....chat.choices.stream.types import AiChoiceDeltas
|
|
20
45
|
from ....chat.choices.stream.types import AiChoicesDeltas
|
|
21
|
-
from ....chat.
|
|
22
|
-
from ....chat.messages import AiMessage
|
|
23
|
-
from ....chat.messages import AnyAiMessage
|
|
24
|
-
from ....chat.messages import Message
|
|
25
|
-
from ....chat.messages import SystemMessage
|
|
26
|
-
from ....chat.messages import UserMessage
|
|
27
|
-
from ....chat.stream.types import ContentAiDelta
|
|
46
|
+
from ....chat.tools.types import Tool
|
|
28
47
|
from ....models.configs import ModelName
|
|
29
48
|
from ....resources import UseResources
|
|
30
49
|
from ....standard import ApiUrl
|
|
31
50
|
from ....stream.services import StreamResponseSink
|
|
32
51
|
from ....stream.services import new_stream_response
|
|
52
|
+
from .protocol import build_mc_ai_choice_deltas
|
|
53
|
+
from .protocol import build_mc_choices_response
|
|
54
|
+
from .protocol import build_ol_request_messages
|
|
55
|
+
from .protocol import build_ol_request_tool
|
|
33
56
|
|
|
34
57
|
|
|
35
58
|
##
|
|
@@ -64,31 +87,6 @@ class BaseOllamaChatChoicesService(lang.Abstract):
|
|
|
64
87
|
self._api_url = cc.pop(self.DEFAULT_API_URL)
|
|
65
88
|
self._model_name = cc.pop(self.DEFAULT_MODEL_NAME)
|
|
66
89
|
|
|
67
|
-
#
|
|
68
|
-
|
|
69
|
-
ROLE_MAP: ta.ClassVar[ta.Mapping[type[Message], pt.Role]] = { # noqa
|
|
70
|
-
SystemMessage: 'system',
|
|
71
|
-
UserMessage: 'user',
|
|
72
|
-
AiMessage: 'assistant',
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
@classmethod
|
|
76
|
-
def _get_message_content(cls, m: Message) -> str | None:
|
|
77
|
-
if isinstance(m, (AiMessage, UserMessage, SystemMessage)):
|
|
78
|
-
return check.isinstance(m.c, str)
|
|
79
|
-
else:
|
|
80
|
-
raise TypeError(m)
|
|
81
|
-
|
|
82
|
-
@classmethod
|
|
83
|
-
def _build_request_messages(cls, mc_msgs: ta.Iterable[Message]) -> ta.Sequence[pt.Message]:
|
|
84
|
-
messages: list[pt.Message] = []
|
|
85
|
-
for m in mc_msgs:
|
|
86
|
-
messages.append(pt.Message(
|
|
87
|
-
role=cls.ROLE_MAP[type(m)],
|
|
88
|
-
content=cls._get_message_content(m),
|
|
89
|
-
))
|
|
90
|
-
return messages
|
|
91
|
-
|
|
92
90
|
|
|
93
91
|
##
|
|
94
92
|
|
|
@@ -103,12 +101,18 @@ class OllamaChatChoicesService(BaseOllamaChatChoicesService):
|
|
|
103
101
|
self,
|
|
104
102
|
request: ChatChoicesRequest,
|
|
105
103
|
) -> ChatChoicesResponse:
|
|
106
|
-
messages =
|
|
104
|
+
messages = build_ol_request_messages(request.v)
|
|
105
|
+
|
|
106
|
+
tools: list[pt.Tool] = []
|
|
107
|
+
with tv.TypedValues(*request.options).consume() as oc:
|
|
108
|
+
t: Tool
|
|
109
|
+
for t in oc.pop(Tool, []):
|
|
110
|
+
tools.append(build_ol_request_tool(t))
|
|
107
111
|
|
|
108
112
|
a_req = pt.ChatRequest(
|
|
109
113
|
model=self._model_name.v,
|
|
110
114
|
messages=messages,
|
|
111
|
-
|
|
115
|
+
tools=tools or None,
|
|
112
116
|
stream=False,
|
|
113
117
|
)
|
|
114
118
|
|
|
@@ -124,17 +128,7 @@ class OllamaChatChoicesService(BaseOllamaChatChoicesService):
|
|
|
124
128
|
|
|
125
129
|
resp = msh.unmarshal(json_response, pt.ChatResponse)
|
|
126
130
|
|
|
127
|
-
|
|
128
|
-
if resp.message.role == 'assistant':
|
|
129
|
-
out.append(AiMessage(
|
|
130
|
-
check.not_none(resp.message.content),
|
|
131
|
-
))
|
|
132
|
-
else:
|
|
133
|
-
raise TypeError(resp.message.role)
|
|
134
|
-
|
|
135
|
-
return ChatChoicesResponse([
|
|
136
|
-
AiChoice(out),
|
|
137
|
-
])
|
|
131
|
+
return build_mc_choices_response(resp)
|
|
138
132
|
|
|
139
133
|
|
|
140
134
|
##
|
|
@@ -152,12 +146,18 @@ class OllamaChatChoicesStreamService(BaseOllamaChatChoicesService):
|
|
|
152
146
|
self,
|
|
153
147
|
request: ChatChoicesStreamRequest,
|
|
154
148
|
) -> ChatChoicesStreamResponse:
|
|
155
|
-
messages =
|
|
149
|
+
messages = build_ol_request_messages(request.v)
|
|
150
|
+
|
|
151
|
+
tools: list[pt.Tool] = []
|
|
152
|
+
with tv.TypedValues(*request.options).consume() as oc:
|
|
153
|
+
t: Tool
|
|
154
|
+
for t in oc.pop(Tool, []):
|
|
155
|
+
tools.append(build_ol_request_tool(t))
|
|
156
156
|
|
|
157
157
|
a_req = pt.ChatRequest(
|
|
158
158
|
model=self._model_name.v,
|
|
159
159
|
messages=messages,
|
|
160
|
-
|
|
160
|
+
tools=tools or None,
|
|
161
161
|
stream=True,
|
|
162
162
|
)
|
|
163
163
|
|
|
@@ -184,14 +184,8 @@ class OllamaChatChoicesStreamService(BaseOllamaChatChoicesService):
|
|
|
184
184
|
lj = json.loads(l.decode('utf-8'))
|
|
185
185
|
lp: pt.ChatResponse = msh.unmarshal(lj, pt.ChatResponse)
|
|
186
186
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
check.state(not lp.message.tool_calls)
|
|
190
|
-
|
|
191
|
-
if (c := lp.message.content):
|
|
192
|
-
await sink.emit(AiChoicesDeltas([AiChoiceDeltas([ContentAiDelta(
|
|
193
|
-
c,
|
|
194
|
-
)])]))
|
|
187
|
+
if (ds := build_mc_ai_choice_deltas(lp)).deltas:
|
|
188
|
+
await sink.emit(AiChoicesDeltas([ds]))
|
|
195
189
|
|
|
196
190
|
if not b:
|
|
197
191
|
return []
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
|
|
3
|
+
from omlish import check
|
|
4
|
+
|
|
5
|
+
from .....backends.ollama import protocol as pt
|
|
6
|
+
from ....chat.choices.services import ChatChoicesResponse
|
|
7
|
+
from ....chat.choices.stream.types import AiChoiceDeltas
|
|
8
|
+
from ....chat.choices.types import AiChoice
|
|
9
|
+
from ....chat.messages import AiMessage
|
|
10
|
+
from ....chat.messages import AnyAiMessage
|
|
11
|
+
from ....chat.messages import Chat
|
|
12
|
+
from ....chat.messages import SystemMessage
|
|
13
|
+
from ....chat.messages import ToolUseMessage
|
|
14
|
+
from ....chat.messages import ToolUseResultMessage
|
|
15
|
+
from ....chat.messages import UserMessage
|
|
16
|
+
from ....chat.stream.types import AiDelta
|
|
17
|
+
from ....chat.stream.types import ContentAiDelta
|
|
18
|
+
from ....chat.stream.types import ToolUseAiDelta
|
|
19
|
+
from ....chat.tools.types import Tool
|
|
20
|
+
from ....content.prepare import prepare_content_str
|
|
21
|
+
from ....tools.jsonschema import build_tool_spec_params_json_schema
|
|
22
|
+
from ....tools.types import ToolUse
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_ol_request_messages(chat: Chat) -> list[pt.Message]:
|
|
29
|
+
ol_msgs: list[pt.Message] = []
|
|
30
|
+
|
|
31
|
+
for _, g in itertools.groupby(chat, lambda mc_m: isinstance(mc_m, AnyAiMessage)):
|
|
32
|
+
mc_msgs = list(g)
|
|
33
|
+
|
|
34
|
+
if isinstance(mc_msgs[0], AnyAiMessage):
|
|
35
|
+
tups: list[tuple[AiMessage | None, list[ToolUseMessage]]] = []
|
|
36
|
+
for mc_msg in mc_msgs:
|
|
37
|
+
if isinstance(mc_msg, AiMessage):
|
|
38
|
+
tups.append((mc_msg, []))
|
|
39
|
+
|
|
40
|
+
elif isinstance(mc_msg, ToolUseMessage):
|
|
41
|
+
if not tups:
|
|
42
|
+
tups.append((None, []))
|
|
43
|
+
tups[-1][1].append(mc_msg)
|
|
44
|
+
|
|
45
|
+
else:
|
|
46
|
+
raise TypeError(mc_msg)
|
|
47
|
+
|
|
48
|
+
for mc_ai_msg, mc_tu_msgs in tups:
|
|
49
|
+
ol_msgs.append(pt.Message(
|
|
50
|
+
role='assistant',
|
|
51
|
+
content=check.isinstance(mc_ai_msg.c, str) if mc_ai_msg is not None else None,
|
|
52
|
+
tool_calls=[
|
|
53
|
+
pt.Message.ToolCall(
|
|
54
|
+
function=pt.Message.ToolCall.Function(
|
|
55
|
+
name=mc_tu_msg.tu.name,
|
|
56
|
+
arguments=mc_tu_msg.tu.args,
|
|
57
|
+
),
|
|
58
|
+
id=check.not_none(mc_tu_msg.tu.id),
|
|
59
|
+
)
|
|
60
|
+
for mc_tu_msg in mc_tu_msgs
|
|
61
|
+
] if mc_tu_msgs else None,
|
|
62
|
+
))
|
|
63
|
+
|
|
64
|
+
else:
|
|
65
|
+
for mc_msg in mc_msgs:
|
|
66
|
+
if isinstance(mc_msg, SystemMessage):
|
|
67
|
+
ol_msgs.append(pt.Message(
|
|
68
|
+
role='system',
|
|
69
|
+
content=check.isinstance(mc_msg.c, str),
|
|
70
|
+
))
|
|
71
|
+
|
|
72
|
+
elif isinstance(mc_msg, UserMessage):
|
|
73
|
+
ol_msgs.append(pt.Message(
|
|
74
|
+
role='user',
|
|
75
|
+
content=check.isinstance(mc_msg.c, str),
|
|
76
|
+
))
|
|
77
|
+
|
|
78
|
+
elif isinstance(mc_msg, ToolUseResultMessage):
|
|
79
|
+
ol_msgs.append(pt.Message(
|
|
80
|
+
role='tool',
|
|
81
|
+
tool_name=mc_msg.tur.name,
|
|
82
|
+
content=check.isinstance(mc_msg.tur.c, str),
|
|
83
|
+
))
|
|
84
|
+
|
|
85
|
+
else:
|
|
86
|
+
raise TypeError(mc_msg)
|
|
87
|
+
|
|
88
|
+
return ol_msgs
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def build_ol_request_tool(t: Tool) -> pt.Tool:
|
|
92
|
+
return pt.Tool(
|
|
93
|
+
function=pt.Tool.Function(
|
|
94
|
+
name=check.not_none(t.spec.name),
|
|
95
|
+
description=prepare_content_str(t.spec.desc),
|
|
96
|
+
parameters=build_tool_spec_params_json_schema(t.spec),
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def build_mc_choices_response(ol_resp: pt.ChatResponse) -> ChatChoicesResponse:
|
|
102
|
+
ol_msg = ol_resp.message
|
|
103
|
+
|
|
104
|
+
lst: list[AnyAiMessage] = []
|
|
105
|
+
|
|
106
|
+
if ol_msg.role in (None, 'assistant'):
|
|
107
|
+
if ol_msg.content is not None:
|
|
108
|
+
lst.append(AiMessage(
|
|
109
|
+
check.isinstance(ol_msg.content, str),
|
|
110
|
+
))
|
|
111
|
+
|
|
112
|
+
for ol_tc in ol_msg.tool_calls or []:
|
|
113
|
+
lst.append(ToolUseMessage(ToolUse(
|
|
114
|
+
id=ol_tc.id,
|
|
115
|
+
name=ol_tc.function.name,
|
|
116
|
+
args=ol_tc.function.arguments,
|
|
117
|
+
)))
|
|
118
|
+
|
|
119
|
+
else:
|
|
120
|
+
raise ValueError(ol_msg)
|
|
121
|
+
|
|
122
|
+
return ChatChoicesResponse([AiChoice(lst)])
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def build_mc_ai_choice_deltas(ol_resp: pt.ChatResponse) -> AiChoiceDeltas:
|
|
126
|
+
ol_msg = ol_resp.message
|
|
127
|
+
|
|
128
|
+
if ol_msg.role in (None, 'assistant'):
|
|
129
|
+
lst: list[AiDelta] = []
|
|
130
|
+
|
|
131
|
+
if ol_msg.content is not None:
|
|
132
|
+
lst.append(ContentAiDelta(ol_msg.content))
|
|
133
|
+
|
|
134
|
+
for tc in ol_msg.tool_calls or []:
|
|
135
|
+
lst.append(ToolUseAiDelta(
|
|
136
|
+
id=tc.id,
|
|
137
|
+
name=check.not_none(tc.function.name),
|
|
138
|
+
args=tc.function.arguments,
|
|
139
|
+
))
|
|
140
|
+
|
|
141
|
+
return AiChoiceDeltas(lst)
|
|
142
|
+
|
|
143
|
+
else:
|
|
144
|
+
raise ValueError(ol_msg)
|