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.
Files changed (88) hide show
  1. ommlds/.omlish-manifests.json +9 -7
  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 +42 -34
  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} +7 -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 +5 -5
  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 +12 -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/inject.py +1 -1
  29. ommlds/cli/sessions/chat/interfaces/textual/app.py +154 -103
  30. ommlds/cli/sessions/chat/interfaces/textual/inject.py +34 -9
  31. ommlds/cli/sessions/chat/interfaces/textual/interface.py +85 -0
  32. ommlds/cli/sessions/chat/interfaces/textual/styles/__init__.py +29 -0
  33. ommlds/cli/sessions/chat/interfaces/textual/styles/input.tcss +3 -1
  34. ommlds/cli/sessions/chat/interfaces/textual/styles/markdown.tcss +7 -0
  35. ommlds/cli/sessions/chat/interfaces/textual/styles/messages.tcss +131 -9
  36. ommlds/cli/sessions/chat/interfaces/textual/tools.py +37 -0
  37. ommlds/cli/sessions/chat/interfaces/textual/widgets/__init__.py +0 -0
  38. ommlds/cli/sessions/chat/interfaces/textual/widgets/input.py +36 -0
  39. ommlds/cli/sessions/chat/interfaces/textual/widgets/messages.py +164 -0
  40. ommlds/minichain/backends/impls/ollama/chat.py +50 -56
  41. ommlds/minichain/backends/impls/ollama/protocol.py +144 -0
  42. ommlds/minichain/backends/impls/openai/names.py +3 -1
  43. ommlds/nanochat/rustbpe/README.md +9 -0
  44. {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev492.dist-info}/METADATA +6 -6
  45. {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev492.dist-info}/RECORD +88 -78
  46. /ommlds/cli/sessions/chat/{agents → drivers}/__init__.py +0 -0
  47. /ommlds/cli/sessions/chat/{agents → drivers}/ai/__init__.py +0 -0
  48. /ommlds/cli/sessions/chat/{agents → drivers}/ai/configs.py +0 -0
  49. /ommlds/cli/sessions/chat/{agents → drivers}/ai/events.py +0 -0
  50. /ommlds/cli/sessions/chat/{agents → drivers}/ai/injection.py +0 -0
  51. /ommlds/cli/sessions/chat/{agents → drivers}/ai/rendering.py +0 -0
  52. /ommlds/cli/sessions/chat/{agents → drivers}/ai/services.py +0 -0
  53. /ommlds/cli/sessions/chat/{agents → drivers}/ai/tools.py +0 -0
  54. /ommlds/cli/sessions/chat/{agents → drivers}/ai/types.py +0 -0
  55. /ommlds/cli/sessions/chat/{agents → drivers}/events/__init__.py +0 -0
  56. /ommlds/cli/sessions/chat/{agents → drivers}/events/inject.py +0 -0
  57. /ommlds/cli/sessions/chat/{agents → drivers}/events/injection.py +0 -0
  58. /ommlds/cli/sessions/chat/{agents → drivers}/events/manager.py +0 -0
  59. /ommlds/cli/sessions/chat/{agents → drivers}/events/types.py +0 -0
  60. /ommlds/cli/sessions/chat/{agents → drivers}/phases/__init__.py +0 -0
  61. /ommlds/cli/sessions/chat/{agents → drivers}/phases/inject.py +0 -0
  62. /ommlds/cli/sessions/chat/{agents → drivers}/phases/injection.py +0 -0
  63. /ommlds/cli/sessions/chat/{agents → drivers}/phases/manager.py +0 -0
  64. /ommlds/cli/sessions/chat/{agents → drivers}/phases/types.py +0 -0
  65. /ommlds/cli/sessions/chat/{agents → drivers}/state/__init__.py +0 -0
  66. /ommlds/cli/sessions/chat/{agents → drivers}/state/configs.py +0 -0
  67. /ommlds/cli/sessions/chat/{agents → drivers}/state/inject.py +0 -0
  68. /ommlds/cli/sessions/chat/{agents → drivers}/state/inmemory.py +0 -0
  69. /ommlds/cli/sessions/chat/{agents → drivers}/state/storage.py +0 -0
  70. /ommlds/cli/sessions/chat/{agents → drivers}/state/types.py +0 -0
  71. /ommlds/cli/sessions/chat/{agents → drivers}/tools/__init__.py +0 -0
  72. /ommlds/cli/sessions/chat/{agents → drivers}/tools/fs/__init__.py +0 -0
  73. /ommlds/cli/sessions/chat/{agents → drivers}/tools/fs/configs.py +0 -0
  74. /ommlds/cli/sessions/chat/{agents → drivers}/tools/fs/inject.py +0 -0
  75. /ommlds/cli/sessions/chat/{agents → drivers}/tools/injection.py +0 -0
  76. /ommlds/cli/sessions/chat/{agents → drivers}/tools/todo/__init__.py +0 -0
  77. /ommlds/cli/sessions/chat/{agents → drivers}/tools/todo/configs.py +0 -0
  78. /ommlds/cli/sessions/chat/{agents → drivers}/tools/todo/inject.py +0 -0
  79. /ommlds/cli/sessions/chat/{agents → drivers}/tools/weather/__init__.py +0 -0
  80. /ommlds/cli/sessions/chat/{agents → drivers}/tools/weather/configs.py +0 -0
  81. /ommlds/cli/sessions/chat/{agents → drivers}/tools/weather/inject.py +0 -0
  82. /ommlds/cli/sessions/chat/{agents → drivers}/tools/weather/tools.py +0 -0
  83. /ommlds/cli/sessions/chat/{agents → drivers}/user/__init__.py +0 -0
  84. /ommlds/cli/sessions/chat/{agents → drivers}/user/configs.py +0 -0
  85. {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev492.dist-info}/WHEEL +0 -0
  86. {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev492.dist-info}/entry_points.txt +0 -0
  87. {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev492.dist-info}/licenses/LICENSE +0 -0
  88. {ommlds-0.0.0.dev490.dist-info → ommlds-0.0.0.dev492.dist-info}/top_level.txt +0 -0
@@ -1,33 +1,155 @@
1
- #messages-scroll {
1
+ /* Container */
2
+
3
+ #messages-container {
2
4
  width: 100%;
3
5
  height: 1fr;
4
6
 
5
- padding: 0 2 0 2;
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
- #messages-container {
16
+
17
+ /* Base */
18
+
19
+ .message {
20
+ width: 1fr;
9
21
  height: auto;
10
- width: 100%;
11
22
 
12
- margin-top: 1;
13
- margin-bottom: 0;
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
- margin: 1;
34
+ padding: 1 2 1 1;
21
35
 
22
36
  border: round;
37
+ }
23
38
 
24
- padding: 1;
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
@@ -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.choices.types import AiChoice
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 = self._build_request_messages(request.v)
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
- # tools=tools or None,
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
- out: list[AnyAiMessage] = []
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 = self._build_request_messages(request.v)
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
- # tools=tools or None,
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
- check.state(lp.message.role == 'assistant')
188
- check.none(lp.message.tool_name)
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)