ommlds 0.0.0.dev515__py3-none-any.whl → 0.0.0.dev516__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/cli/profiles.py ADDED
@@ -0,0 +1,449 @@
1
+ import abc
2
+ import sys
3
+ import typing as ta
4
+
5
+ from omlish import check
6
+ from omlish import dataclasses as dc
7
+ from omlish import lang
8
+ from omlish.argparse import all as ap
9
+
10
+ from .sessions.chat.configs import ChatConfig
11
+ from .sessions.chat.interfaces.bare.configs import BareInterfaceConfig
12
+ from .sessions.chat.interfaces.configs import InterfaceConfig
13
+ from .sessions.chat.interfaces.textual.configs import TextualInterfaceConfig
14
+ from .sessions.completion.configs import CompletionConfig
15
+ from .sessions.configs import SessionConfig
16
+ from .sessions.embedding.configs import EmbeddingConfig
17
+
18
+
19
+ SessionConfigT = ta.TypeVar('SessionConfigT', bound=SessionConfig)
20
+ SessionConfigU = ta.TypeVar('SessionConfigU', bound=SessionConfig)
21
+
22
+
23
+ ##
24
+
25
+
26
+ class Profile(lang.Abstract, ta.Generic[SessionConfigT]):
27
+ @abc.abstractmethod
28
+ def configure(self, argv: ta.Sequence[str]) -> SessionConfigT:
29
+ raise NotImplementedError
30
+
31
+
32
+ ##
33
+
34
+
35
+ class ProfileAspect(lang.Abstract, ta.Generic[SessionConfigT]):
36
+ @property
37
+ def name(self) -> str:
38
+ return lang.camel_to_snake(type(self).__name__).lower()
39
+
40
+ @property
41
+ def default_parser_arg_group(self) -> str | None:
42
+ return self.name
43
+
44
+ @property
45
+ def parser_args(self) -> ta.Sequence[ap.Arg]:
46
+ return []
47
+
48
+ @ta.final
49
+ @dc.dataclass(frozen=True)
50
+ class ConfigureContext(ta.Generic[SessionConfigU]):
51
+ profile: 'Profile[SessionConfigU]'
52
+ args: ap.Namespace
53
+
54
+ @abc.abstractmethod
55
+ def configure(self, ctx: ConfigureContext[SessionConfigT], cfg: SessionConfigT) -> SessionConfigT:
56
+ raise NotImplementedError
57
+
58
+
59
+ class AspectProfile(Profile[SessionConfigT], lang.Abstract):
60
+ @abc.abstractmethod
61
+ def _build_aspects(self) -> ta.Sequence[ProfileAspect[SessionConfigT]]:
62
+ return []
63
+
64
+ __aspects: ta.Sequence[ProfileAspect[SessionConfigT]]
65
+
66
+ @ta.final
67
+ @property
68
+ def aspects(self) -> ta.Sequence[ProfileAspect[SessionConfigT]]:
69
+ try:
70
+ return self.__aspects
71
+ except AttributeError:
72
+ pass
73
+ self.__aspects = aspects = tuple(self._build_aspects())
74
+ return aspects
75
+
76
+ #
77
+
78
+ @abc.abstractmethod
79
+ def initial_config(self) -> SessionConfigT:
80
+ raise NotImplementedError
81
+
82
+ #
83
+
84
+ def configure(self, argv: ta.Sequence[str]) -> SessionConfigT:
85
+ parser = ap.ArgumentParser()
86
+
87
+ pa_grps: dict[str, ta.Any] = {}
88
+ for a in self.aspects:
89
+ for pa in a.parser_args:
90
+ if (pa_gn := lang.opt_coalesce(pa.group, a.default_parser_arg_group)) is not None:
91
+ check.non_empty_str(pa_gn)
92
+ try:
93
+ pa_grp = pa_grps[pa_gn]
94
+ except KeyError:
95
+ pa_grps[pa_gn] = pa_grp = parser.add_argument_group(pa_gn)
96
+ pa_grp.add_argument(*pa.args, **pa.kwargs)
97
+ else:
98
+ parser.add_argument(*pa.args, **pa.kwargs)
99
+
100
+ args = parser.parse_args(argv)
101
+
102
+ cfg_ctx = ProfileAspect.ConfigureContext(
103
+ self,
104
+ args,
105
+ )
106
+ cfg = self.initial_config()
107
+ for a in self.aspects:
108
+ cfg = a.configure(cfg_ctx, cfg)
109
+
110
+ return cfg
111
+
112
+
113
+ ##
114
+
115
+
116
+ class ChatProfile(AspectProfile[ChatConfig]):
117
+ class Backend(ProfileAspect[ChatConfig]):
118
+ parser_args: ta.ClassVar[ta.Sequence[ap.Arg]] = [
119
+ ap.arg('-b', '--backend'),
120
+ ]
121
+
122
+ def configure(self, ctx: ProfileAspect.ConfigureContext[ChatConfig], cfg: ChatConfig) -> ChatConfig:
123
+ return dc.replace(
124
+ cfg,
125
+ driver=dc.replace(
126
+ cfg.driver,
127
+ backend=dc.replace(
128
+ cfg.driver.backend,
129
+ backend=ctx.args.backend,
130
+ ),
131
+ ),
132
+ )
133
+
134
+ #
135
+
136
+ class Interface(ProfileAspect[ChatConfig]):
137
+ parser_args: ta.ClassVar[ta.Sequence[ap.Arg]] = [
138
+ ap.arg('-i', '--interactive', action='store_true'),
139
+ ap.arg('-T', '--textual', action='store_true'),
140
+ ap.arg('-e', '--editor', action='store_true'),
141
+ ]
142
+
143
+ def configure(self, ctx: ProfileAspect.ConfigureContext[ChatConfig], cfg: ChatConfig) -> ChatConfig:
144
+ if ctx.args.editor:
145
+ check.arg(not ctx.args.interactive)
146
+ check.arg(not ctx.args.message)
147
+ raise NotImplementedError
148
+
149
+ if ctx.args.textual:
150
+ check.isinstance(cfg.interface, BareInterfaceConfig)
151
+ cfg = dc.replace(
152
+ cfg,
153
+ interface=TextualInterfaceConfig(**{
154
+ f.name: getattr(cfg.interface, f.name)
155
+ for f in dc.fields(InterfaceConfig)
156
+ }),
157
+ )
158
+
159
+ else:
160
+ cfg = dc.replace(
161
+ cfg,
162
+ driver=dc.replace(
163
+ cfg.driver,
164
+ ai=dc.replace(
165
+ cfg.driver.ai,
166
+ verbose=True,
167
+ ),
168
+ ),
169
+ interface=dc.replace(
170
+ check.isinstance(cfg.interface, BareInterfaceConfig),
171
+ interactive=ctx.args.interactive,
172
+ ),
173
+ )
174
+
175
+ return cfg
176
+
177
+ #
178
+
179
+ class Input(ProfileAspect[ChatConfig]):
180
+ parser_args: ta.ClassVar[ta.Sequence[ap.Arg]] = [
181
+ ap.arg('message', nargs='*'),
182
+ ]
183
+
184
+ def configure(self, ctx: ProfileAspect.ConfigureContext[ChatConfig], cfg: ChatConfig) -> ChatConfig:
185
+ if ctx.args.interactive or ctx.args.textual:
186
+ check.arg(not ctx.args.message)
187
+
188
+ elif ctx.args.message:
189
+ ps: list[str] = []
190
+
191
+ for a in ctx.args.message:
192
+ if a == '-':
193
+ ps.append(sys.stdin.read())
194
+
195
+ elif a.startswith('@'):
196
+ with open(a[1:]) as f:
197
+ ps.append(f.read())
198
+
199
+ else:
200
+ ps.append(a)
201
+
202
+ c = ' '.join(ps)
203
+
204
+ cfg = dc.replace(
205
+ cfg,
206
+ driver=dc.replace(
207
+ cfg.driver,
208
+ user=dc.replace(
209
+ cfg.driver.user,
210
+ initial_user_content=c,
211
+ ),
212
+ ),
213
+ )
214
+
215
+ else:
216
+ raise ValueError('Must specify input')
217
+
218
+ return cfg
219
+
220
+ #
221
+
222
+ class State(ProfileAspect[ChatConfig]):
223
+ parser_args: ta.ClassVar[ta.Sequence[ap.Arg]] = [
224
+ ap.arg('-n', '--new', action='store_true'),
225
+ ap.arg('--ephemeral', action='store_true'),
226
+ ]
227
+
228
+ def configure(self, ctx: ProfileAspect.ConfigureContext[ChatConfig], cfg: ChatConfig) -> ChatConfig:
229
+ return dc.replace(
230
+ cfg,
231
+ driver=dc.replace(
232
+ cfg.driver,
233
+ state=dc.replace(
234
+ cfg.driver.state,
235
+ state='ephemeral' if ctx.args.ephemeral else 'new' if ctx.args.new else 'continue',
236
+ ),
237
+ ),
238
+ )
239
+
240
+ #
241
+
242
+ class Output(ProfileAspect[ChatConfig]):
243
+ parser_args: ta.ClassVar[ta.Sequence[ap.Arg]] = [
244
+ ap.arg('-s', '--stream', action='store_true'),
245
+ ap.arg('-M', '--markdown', action='store_true'),
246
+ ]
247
+
248
+ def configure(self, ctx: ProfileAspect.ConfigureContext[ChatConfig], cfg: ChatConfig) -> ChatConfig:
249
+ return dc.replace(
250
+ cfg,
251
+ driver=dc.replace(
252
+ cfg.driver,
253
+ ai=dc.replace(
254
+ cfg.driver.ai,
255
+ stream=bool(ctx.args.stream),
256
+ ),
257
+ ),
258
+ rendering=dc.replace(
259
+ cfg.rendering,
260
+ markdown=bool(ctx.args.markdown),
261
+ ),
262
+ )
263
+
264
+ #
265
+
266
+ class Tools(ProfileAspect[ChatConfig]):
267
+ parser_args: ta.ClassVar[ta.Sequence[ap.Arg]] = [
268
+ ap.arg('--enable-fs-tools', action='store_true'),
269
+ ap.arg('--enable-todo-tools', action='store_true'),
270
+ # ap.arg('--enable-unsafe-tools-do-not-use-lol', action='store_true'),
271
+ ap.arg('--enable-test-weather-tool', action='store_true'),
272
+ ]
273
+
274
+ def configure_with_tools(
275
+ self,
276
+ ctx: ProfileAspect.ConfigureContext[ChatConfig],
277
+ cfg: ChatConfig,
278
+ enabled_tools: ta.Iterable[str],
279
+ ) -> ChatConfig:
280
+ check.not_isinstance(enabled_tools, str)
281
+
282
+ return dc.replace(
283
+ cfg,
284
+ driver=dc.replace(
285
+ cfg.driver,
286
+ ai=dc.replace(
287
+ cfg.driver.ai,
288
+ enable_tools=True,
289
+ ),
290
+ tools=dc.replace(
291
+ cfg.driver.tools,
292
+ enabled_tools={ # noqa
293
+ *(cfg.driver.tools.enabled_tools or []),
294
+ *enabled_tools,
295
+ },
296
+ ),
297
+ ),
298
+ interface=dc.replace(
299
+ cfg.interface,
300
+ enable_tools=True,
301
+ ),
302
+ )
303
+
304
+ def configure(self, ctx: ProfileAspect.ConfigureContext[ChatConfig], cfg: ChatConfig) -> ChatConfig:
305
+ if not (
306
+ ctx.args.enable_fs_tools or
307
+ ctx.args.enable_todo_tools or
308
+ # ctx.args.enable_unsafe_tools_do_not_use_lol or
309
+ ctx.args.enable_test_weather_tool
310
+ ):
311
+ return cfg
312
+
313
+ return self.configure_with_tools(ctx, cfg, {
314
+ *(['fs'] if ctx.args.enable_fs_tools else []),
315
+ *(['todo'] if ctx.args.enable_todo_tools else []),
316
+ *(['weather'] if ctx.args.enable_test_weather_tool else []),
317
+ })
318
+
319
+ #
320
+
321
+ class Code(ProfileAspect[ChatConfig]):
322
+ parser_args: ta.ClassVar[ta.Sequence[ap.Arg]] = [
323
+ ap.arg('-c', '--code', action='store_true'),
324
+ ]
325
+
326
+ def configure_for_code(self, ctx: ProfileAspect.ConfigureContext[ChatConfig], cfg: ChatConfig) -> ChatConfig:
327
+ cfg = dc.replace(
328
+ cfg,
329
+ driver=dc.replace(
330
+ cfg.driver,
331
+ ai=dc.replace(
332
+ cfg.driver.ai,
333
+ enable_tools=True,
334
+ ),
335
+ ),
336
+ )
337
+
338
+ if ctx.args.new or ctx.args.ephemeral:
339
+ from ..minichain.lib.code.prompts import CODE_AGENT_SYSTEM_PROMPT
340
+ system_content = CODE_AGENT_SYSTEM_PROMPT
341
+
342
+ cfg = dc.replace(
343
+ cfg,
344
+ driver=dc.replace(
345
+ cfg.driver,
346
+ user=dc.replace(
347
+ cfg.driver.user,
348
+ initial_system_content=system_content,
349
+ ),
350
+ ),
351
+ )
352
+
353
+ return cfg
354
+
355
+ def configure(self, ctx: ProfileAspect.ConfigureContext[ChatConfig], cfg: ChatConfig) -> ChatConfig:
356
+ if not ctx.args.code:
357
+ return cfg
358
+
359
+ return self.configure_for_code(ctx, cfg)
360
+
361
+ #
362
+
363
+ def _build_aspects(self) -> ta.Sequence[ProfileAspect[ChatConfig]]:
364
+ return [
365
+ *super()._build_aspects(),
366
+ self.Backend(),
367
+ self.Interface(),
368
+ self.Input(),
369
+ self.State(),
370
+ self.Output(),
371
+ self.Tools(),
372
+ self.Code(),
373
+ ]
374
+
375
+ def initial_config(self) -> ChatConfig:
376
+ return ChatConfig()
377
+
378
+
379
+ #
380
+
381
+
382
+ class CodeProfile(ChatProfile):
383
+ class Tools(ChatProfile.Tools):
384
+ parser_args: ta.ClassVar[ta.Sequence[ap.Arg]] = []
385
+
386
+ def configure(self, ctx: ProfileAspect.ConfigureContext[ChatConfig], cfg: ChatConfig) -> ChatConfig:
387
+ return self.configure_with_tools(ctx, cfg, {
388
+ 'fs',
389
+ 'todo',
390
+ })
391
+
392
+ class Code(ChatProfile.Code):
393
+ parser_args: ta.ClassVar[ta.Sequence[ap.Arg]] = []
394
+
395
+ def configure(self, ctx: ProfileAspect.ConfigureContext[ChatConfig], cfg: ChatConfig) -> ChatConfig:
396
+ return self.configure_for_code(ctx, cfg)
397
+
398
+
399
+ ##
400
+
401
+
402
+ class CompletionProfile(Profile):
403
+ def configure(self, argv: ta.Sequence[str]) -> SessionConfig:
404
+ parser = ap.ArgumentParser()
405
+ parser.add_argument('prompt', nargs='*')
406
+ parser.add_argument('-b', '--backend', default='openai')
407
+ args = parser.parse_args(argv)
408
+
409
+ content = ' '.join(args.prompt)
410
+
411
+ cfg = CompletionConfig(
412
+ content=check.non_empty_str(content),
413
+ backend=args.backend,
414
+ )
415
+
416
+ return cfg
417
+
418
+
419
+ ##
420
+
421
+
422
+ class EmbedProfile(Profile):
423
+ def configure(self, argv: ta.Sequence[str]) -> SessionConfig:
424
+ parser = ap.ArgumentParser()
425
+ parser.add_argument('prompt', nargs='*')
426
+ parser.add_argument('-b', '--backend', default='openai')
427
+ args = parser.parse_args(argv)
428
+
429
+ content = ' '.join(args.prompt)
430
+
431
+ cfg = EmbeddingConfig(
432
+ content=check.non_empty_str(content),
433
+ backend=args.backend,
434
+ )
435
+
436
+ return cfg
437
+
438
+
439
+ ##
440
+
441
+
442
+ PROFILE_TYPES: ta.Mapping[str, type[Profile]] = {
443
+ 'chat': ChatProfile,
444
+ 'code': CodeProfile,
445
+
446
+ 'complete': CompletionProfile,
447
+
448
+ 'embed': EmbedProfile,
449
+ }
@@ -16,6 +16,7 @@ from omlish.logs import all as logs
16
16
 
17
17
  from ...... import minichain as mc
18
18
  from .....backends.types import BackendName
19
+ from ....types import SessionProfileName
19
20
  from ...drivers.events.types import AiDeltaChatEvent
20
21
  from ...drivers.events.types import AiMessagesChatEvent
21
22
  from ...drivers.types import ChatDriver
@@ -100,6 +101,7 @@ class ChatApp(
100
101
  backend_name: BackendName | None = None,
101
102
  devtools_setup: tx.DevtoolsSetup | None = None,
102
103
  input_history_manager: InputHistoryManager,
104
+ session_profile_name: SessionProfileName | None = None,
103
105
  ) -> None:
104
106
  super().__init__()
105
107
 
@@ -111,6 +113,7 @@ class ChatApp(
111
113
  self._chat_event_queue = chat_event_queue
112
114
  self._backend_name = backend_name
113
115
  self._input_history_manager = input_history_manager
116
+ self._session_profile_name = session_profile_name
114
117
 
115
118
  self._chat_action_queue: asyncio.Queue[ta.Any] = asyncio.Queue()
116
119
 
@@ -328,6 +331,7 @@ class ChatApp(
328
331
 
329
332
  await self._mount_messages(
330
333
  WelcomeMessage('\n'.join([
334
+ *([f'Profile: {self._session_profile_name}'] if self._session_profile_name is not None else []),
331
335
  f'Backend: {self._backend_name or "?"}',
332
336
  f'Dir: {os.getcwd()}',
333
337
  ])),
@@ -5,6 +5,7 @@ from .chat.configs import ChatConfig
5
5
  from .completion.configs import CompletionConfig
6
6
  from .configs import SessionConfig
7
7
  from .embedding.configs import EmbeddingConfig
8
+ from .types import SessionProfileName
8
9
 
9
10
 
10
11
  with lang.auto_proxy_import(globals()):
@@ -16,11 +17,20 @@ with lang.auto_proxy_import(globals()):
16
17
  ##
17
18
 
18
19
 
19
- def bind_sessions(cfg: SessionConfig) -> inj.Elements:
20
+ def bind_sessions(
21
+ cfg: SessionConfig,
22
+ *,
23
+ profile_name: str | None = None,
24
+ ) -> inj.Elements:
20
25
  els: list[inj.Elemental] = []
21
26
 
22
27
  #
23
28
 
29
+ if profile_name is not None:
30
+ els.append(inj.bind(SessionProfileName, to_const=profile_name))
31
+
32
+ #
33
+
24
34
  if isinstance(cfg, ChatConfig):
25
35
  els.append(_chat.bind_chat(cfg))
26
36
 
@@ -0,0 +1,7 @@
1
+ import typing as ta
2
+
3
+
4
+ ##
5
+
6
+
7
+ SessionProfileName = ta.NewType('SessionProfileName', str)
@@ -69,13 +69,15 @@ class TransformersTokenizer(tks.BaseTokenizer):
69
69
  ) -> list[tks.Token]:
70
70
  ts = self._tfm_tokenizer.tokenize(text)
71
71
  ids = self._tfm_tokenizer.convert_tokens_to_ids(ts)
72
- return ids
72
+ return ta.cast('list[tks.Token]', ids)
73
73
 
74
74
  def _decode(
75
75
  self,
76
76
  tokens: ta.Iterable[tks.Token],
77
77
  ) -> str:
78
- return self._tfm_tokenizer.decode(tokens) # type: ignore[arg-type]
78
+ if not isinstance(tokens, (tuple, list)): # Yes actually special cased by transformers
79
+ tokens = list(tokens)
80
+ return check.isinstance(self._tfm_tokenizer.decode(ta.cast('list[int]', tokens)), str)
79
81
 
80
82
 
81
83
  ##
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ommlds
3
- Version: 0.0.0.dev515
3
+ Version: 0.0.0.dev516
4
4
  Summary: ommlds
5
5
  Author: wrmsr
6
6
  License-Expression: BSD-3-Clause
@@ -14,20 +14,20 @@ Classifier: Programming Language :: Python :: 3.13
14
14
  Requires-Python: >=3.13
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
- Requires-Dist: omlish==0.0.0.dev515
17
+ Requires-Dist: omlish==0.0.0.dev516
18
18
  Provides-Extra: all
19
- Requires-Dist: omdev==0.0.0.dev515; extra == "all"
19
+ Requires-Dist: omdev==0.0.0.dev516; extra == "all"
20
20
  Requires-Dist: llama-cpp-python~=0.3; extra == "all"
21
21
  Requires-Dist: mlx~=0.30; sys_platform == "darwin" and extra == "all"
22
- Requires-Dist: mlx-lm~=0.29; sys_platform == "darwin" and extra == "all"
22
+ Requires-Dist: mlx-lm~=0.30; sys_platform == "darwin" and extra == "all"
23
23
  Requires-Dist: sentencepiece~=0.2; extra == "all"
24
24
  Requires-Dist: tiktoken~=0.12; extra == "all"
25
25
  Requires-Dist: tinygrad~=0.12; extra == "all"
26
26
  Requires-Dist: tokenizers~=0.22; extra == "all"
27
27
  Requires-Dist: torch~=2.10; extra == "all"
28
- Requires-Dist: transformers~=4.57; extra == "all"
28
+ Requires-Dist: transformers~=5.0; extra == "all"
29
29
  Requires-Dist: sentence-transformers~=5.2; extra == "all"
30
- Requires-Dist: huggingface-hub~=0.36; extra == "all"
30
+ Requires-Dist: huggingface-hub~=1.3; extra == "all"
31
31
  Requires-Dist: datasets~=4.5; extra == "all"
32
32
  Requires-Dist: regex>=2026.1; extra == "all"
33
33
  Requires-Dist: numpy>=1.26; extra == "all"
@@ -39,20 +39,20 @@ Requires-Dist: mwparserfromhell~=0.7; extra == "all"
39
39
  Requires-Dist: wikitextparser~=0.56; extra == "all"
40
40
  Requires-Dist: lxml>=5.3; python_version < "3.13" and extra == "all"
41
41
  Provides-Extra: omdev
42
- Requires-Dist: omdev==0.0.0.dev515; extra == "omdev"
42
+ Requires-Dist: omdev==0.0.0.dev516; extra == "omdev"
43
43
  Provides-Extra: backends
44
44
  Requires-Dist: llama-cpp-python~=0.3; extra == "backends"
45
45
  Requires-Dist: mlx~=0.30; sys_platform == "darwin" and extra == "backends"
46
- Requires-Dist: mlx-lm~=0.29; sys_platform == "darwin" and extra == "backends"
46
+ Requires-Dist: mlx-lm~=0.30; sys_platform == "darwin" and extra == "backends"
47
47
  Requires-Dist: sentencepiece~=0.2; extra == "backends"
48
48
  Requires-Dist: tiktoken~=0.12; extra == "backends"
49
49
  Requires-Dist: tinygrad~=0.12; extra == "backends"
50
50
  Requires-Dist: tokenizers~=0.22; extra == "backends"
51
51
  Requires-Dist: torch~=2.10; extra == "backends"
52
- Requires-Dist: transformers~=4.57; extra == "backends"
52
+ Requires-Dist: transformers~=5.0; extra == "backends"
53
53
  Requires-Dist: sentence-transformers~=5.2; extra == "backends"
54
54
  Provides-Extra: huggingface
55
- Requires-Dist: huggingface-hub~=0.36; extra == "huggingface"
55
+ Requires-Dist: huggingface-hub~=1.3; extra == "huggingface"
56
56
  Requires-Dist: datasets~=4.5; extra == "huggingface"
57
57
  Provides-Extra: nanochat
58
58
  Requires-Dist: regex>=2026.1; extra == "nanochat"