ommlds 0.0.0.dev475__py3-none-any.whl → 0.0.0.dev477__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 +85 -1
  2. ommlds/__about__.py +1 -1
  3. ommlds/backends/groq/_marshal.py +23 -0
  4. ommlds/backends/groq/protocol.py +249 -0
  5. ommlds/cli/{sessions/chat/backends → backends}/catalog.py +35 -3
  6. ommlds/cli/backends/configs.py +9 -0
  7. ommlds/cli/backends/inject.py +31 -36
  8. ommlds/cli/{sessions/chat/backends → backends}/injection.py +1 -1
  9. ommlds/cli/{sessions/chat/backends → backends}/types.py +11 -1
  10. ommlds/cli/{sessions/chat/content → content}/messages.py +1 -1
  11. ommlds/cli/{sessions/chat/content → content}/strings.py +1 -1
  12. ommlds/cli/inject.py +0 -6
  13. ommlds/cli/inputs/asyncs.py +32 -0
  14. ommlds/cli/{sessions/chat/chat/user/inputs.py → inputs/sync.py} +0 -30
  15. ommlds/cli/main.py +267 -113
  16. ommlds/cli/rendering/__init__.py +0 -0
  17. ommlds/cli/rendering/configs.py +9 -0
  18. ommlds/cli/{sessions/chat/rendering → rendering}/inject.py +4 -5
  19. ommlds/cli/{sessions/chat/rendering → rendering}/markdown.py +1 -1
  20. ommlds/cli/{sessions/chat/rendering → rendering}/raw.py +1 -1
  21. ommlds/cli/{sessions/chat/rendering → rendering}/types.py +1 -1
  22. ommlds/cli/secrets.py +21 -0
  23. ommlds/cli/sessions/base.py +1 -1
  24. ommlds/cli/sessions/chat/chat/ai/configs.py +11 -0
  25. ommlds/cli/sessions/chat/chat/ai/inject.py +7 -11
  26. ommlds/cli/sessions/chat/chat/ai/rendering.py +4 -4
  27. ommlds/cli/sessions/chat/chat/ai/services.py +2 -2
  28. ommlds/cli/sessions/chat/chat/state/configs.py +11 -0
  29. ommlds/cli/sessions/chat/chat/state/inject.py +6 -10
  30. ommlds/cli/sessions/chat/chat/state/inmemory.py +1 -2
  31. ommlds/cli/sessions/chat/chat/state/storage.py +1 -2
  32. ommlds/cli/sessions/chat/chat/state/types.py +1 -1
  33. ommlds/cli/sessions/chat/chat/user/configs.py +17 -0
  34. ommlds/cli/sessions/chat/chat/user/inject.py +13 -19
  35. ommlds/cli/sessions/chat/chat/user/interactive.py +3 -3
  36. ommlds/cli/sessions/chat/configs.py +15 -26
  37. ommlds/cli/sessions/chat/inject.py +18 -35
  38. ommlds/cli/sessions/chat/session.py +1 -1
  39. ommlds/cli/sessions/chat/tools/configs.py +22 -0
  40. ommlds/cli/sessions/chat/tools/fs/__init__.py +0 -0
  41. ommlds/cli/sessions/chat/tools/fs/configs.py +12 -0
  42. ommlds/cli/sessions/chat/tools/fs/inject.py +35 -0
  43. ommlds/cli/sessions/chat/tools/inject.py +17 -74
  44. ommlds/cli/sessions/chat/tools/injection.py +15 -0
  45. ommlds/cli/sessions/chat/tools/rendering.py +1 -1
  46. ommlds/cli/sessions/chat/tools/todo/__init__.py +0 -0
  47. ommlds/cli/sessions/chat/tools/todo/configs.py +12 -0
  48. ommlds/cli/sessions/chat/tools/todo/inject.py +31 -0
  49. ommlds/cli/sessions/chat/tools/weather/__init__.py +0 -0
  50. ommlds/cli/sessions/chat/tools/weather/configs.py +12 -0
  51. ommlds/cli/sessions/chat/tools/weather/inject.py +22 -0
  52. ommlds/cli/sessions/chat/tools/{weather.py → weather/tools.py} +1 -1
  53. ommlds/cli/sessions/completion/configs.py +2 -2
  54. ommlds/cli/sessions/completion/inject.py +14 -0
  55. ommlds/cli/sessions/completion/session.py +7 -11
  56. ommlds/cli/sessions/embedding/configs.py +2 -2
  57. ommlds/cli/sessions/embedding/inject.py +14 -0
  58. ommlds/cli/sessions/embedding/session.py +7 -11
  59. ommlds/cli/state/storage.py +1 -1
  60. ommlds/minichain/backends/catalogs/strings.py +1 -1
  61. ommlds/minichain/backends/impls/groq/__init__.py +0 -0
  62. ommlds/minichain/backends/impls/groq/chat.py +75 -0
  63. ommlds/minichain/backends/impls/groq/names.py +48 -0
  64. ommlds/minichain/backends/impls/groq/protocol.py +143 -0
  65. ommlds/minichain/backends/impls/groq/stream.py +125 -0
  66. ommlds/minichain/backends/impls/openai/chat.py +3 -3
  67. ommlds/minichain/backends/impls/openai/names.py +27 -3
  68. ommlds/minichain/backends/impls/openai/stream.py +2 -2
  69. ommlds/minichain/chat/stream/joining.py +1 -0
  70. ommlds/minichain/tools/reflect.py +5 -1
  71. ommlds/wiki/utils/xml.py +5 -5
  72. {ommlds-0.0.0.dev475.dist-info → ommlds-0.0.0.dev477.dist-info}/METADATA +5 -5
  73. {ommlds-0.0.0.dev475.dist-info → ommlds-0.0.0.dev477.dist-info}/RECORD +80 -58
  74. ommlds/cli/backends/standard.py +0 -20
  75. ommlds/cli/main2.py +0 -220
  76. ommlds/cli/sessions/chat/backends/inject.py +0 -53
  77. /ommlds/{cli/sessions/chat/backends → backends/groq}/__init__.py +0 -0
  78. /ommlds/cli/{sessions/chat/content → content}/__init__.py +0 -0
  79. /ommlds/cli/{sessions/chat/rendering → inputs}/__init__.py +0 -0
  80. {ommlds-0.0.0.dev475.dist-info → ommlds-0.0.0.dev477.dist-info}/WHEEL +0 -0
  81. {ommlds-0.0.0.dev475.dist-info → ommlds-0.0.0.dev477.dist-info}/entry_points.txt +0 -0
  82. {ommlds-0.0.0.dev475.dist-info → ommlds-0.0.0.dev477.dist-info}/licenses/LICENSE +0 -0
  83. {ommlds-0.0.0.dev475.dist-info → ommlds-0.0.0.dev477.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,12 @@
1
+ from omlish import dataclasses as dc
2
+ from omlish import lang
3
+
4
+ from ..configs import ToolSetConfig
5
+
6
+
7
+ ##
8
+
9
+
10
+ @dc.dataclass(frozen=True, kw_only=True)
11
+ class TodoToolSetConfig(ToolSetConfig, lang.Final):
12
+ pass
@@ -0,0 +1,31 @@
1
+ from omlish import inject as inj
2
+
3
+ from ..injection import ToolSetBinder
4
+ from ..injection import bind_tool_context_provider_to_key
5
+ from ..injection import tool_catalog_entries
6
+ from .configs import TodoToolSetConfig
7
+
8
+
9
+ ##
10
+
11
+
12
+ def bind_todo_tools(cfg: TodoToolSetConfig) -> inj.Elements:
13
+ from ......minichain.lib.todo.context import TodoContext
14
+ from ......minichain.lib.todo.tools.read import todo_read_tool
15
+ from ......minichain.lib.todo.tools.write import todo_write_tool
16
+
17
+ return inj.as_elements(
18
+ tool_catalog_entries().bind_item_consts(
19
+ todo_read_tool(),
20
+ todo_write_tool(),
21
+ ),
22
+
23
+ inj.bind(TodoContext()),
24
+ bind_tool_context_provider_to_key(TodoContext),
25
+ )
26
+
27
+
28
+ ##
29
+
30
+
31
+ TODO_TOOL_SET_BINDER = ToolSetBinder(TodoToolSetConfig, bind_todo_tools)
File without changes
@@ -0,0 +1,12 @@
1
+ from omlish import dataclasses as dc
2
+ from omlish import lang
3
+
4
+ from ..configs import ToolSetConfig
5
+
6
+
7
+ ##
8
+
9
+
10
+ @dc.dataclass(frozen=True, kw_only=True)
11
+ class WeatherToolSetConfig(ToolSetConfig, lang.Final):
12
+ pass
@@ -0,0 +1,22 @@
1
+ from omlish import inject as inj
2
+
3
+ from ..injection import ToolSetBinder
4
+ from ..injection import tool_catalog_entries
5
+ from .configs import WeatherToolSetConfig
6
+
7
+
8
+ ##
9
+
10
+
11
+ def bind_weather_tools(cfg: WeatherToolSetConfig) -> inj.Elements:
12
+ from .tools import WEATHER_TOOL
13
+
14
+ return inj.as_elements(
15
+ tool_catalog_entries().bind_item_consts(WEATHER_TOOL),
16
+ )
17
+
18
+
19
+ ##
20
+
21
+
22
+ WEATHER_TOOL_SET_BINDER = ToolSetBinder(WeatherToolSetConfig, bind_weather_tools)
@@ -1,4 +1,4 @@
1
- from ..... import minichain as mc
1
+ from ...... import minichain as mc
2
2
 
3
3
 
4
4
  ##
@@ -1,4 +1,4 @@
1
- import dataclasses as dc
1
+ from omlish import dataclasses as dc
2
2
 
3
3
  from .... import minichain as mc
4
4
 
@@ -6,7 +6,7 @@ from .... import minichain as mc
6
6
  ##
7
7
 
8
8
 
9
- DEFAULT_COMPLETION_MODEL_BACKEND = 'openai'
9
+ DEFAULT_BACKEND = 'openai'
10
10
 
11
11
 
12
12
  ##
@@ -2,11 +2,15 @@ from omlish import dataclasses as dc
2
2
  from omlish import inject as inj
3
3
  from omlish import lang
4
4
 
5
+ from ...backends.configs import BackendConfig
6
+ from ...backends.types import DefaultBackendName
5
7
  from ..base import Session
8
+ from .configs import DEFAULT_BACKEND
6
9
  from .configs import CompletionConfig
7
10
 
8
11
 
9
12
  with lang.auto_proxy_import(globals()):
13
+ from ...backends import inject as _backends
10
14
  from . import session as _session
11
15
 
12
16
 
@@ -25,4 +29,14 @@ def bind_completion(cfg: CompletionConfig) -> inj.Elements:
25
29
 
26
30
  #
27
31
 
32
+ els.extend([
33
+ _backends.bind_backends(BackendConfig(
34
+ backend=cfg.backend,
35
+ )),
36
+
37
+ inj.bind(DefaultBackendName, to_const=DEFAULT_BACKEND),
38
+ ])
39
+
40
+ #
41
+
28
42
  return inj.as_elements(*els)
@@ -1,11 +1,9 @@
1
- import dataclasses as dc
2
-
3
1
  from omlish import check
4
- from omlish import lang
2
+ from omlish import dataclasses as dc
5
3
 
6
4
  from .... import minichain as mc
5
+ from ...backends.types import CompletionServiceBackendProvider
7
6
  from ..base import Session
8
- from .configs import DEFAULT_COMPLETION_MODEL_BACKEND
9
7
  from .configs import CompletionConfig
10
8
 
11
9
 
@@ -21,19 +19,17 @@ class CompletionSession(Session['CompletionSession.Config']):
21
19
  self,
22
20
  config: Config,
23
21
  *,
24
- backend_catalog: mc.BackendCatalog,
22
+ service_provider: CompletionServiceBackendProvider,
25
23
  ) -> None:
26
24
  super().__init__(config)
27
25
 
28
- self._backend_catalog = backend_catalog
26
+ self._service_provider = service_provider
29
27
 
30
28
  async def run(self) -> None:
31
29
  prompt = check.isinstance(self._config.content, str)
32
30
 
33
31
  mdl: mc.CompletionService
34
- async with lang.async_maybe_managing(self._backend_catalog.new_backend(
35
- mc.CompletionService,
36
- self._config.backend or DEFAULT_COMPLETION_MODEL_BACKEND,
37
- )) as mdl:
32
+ async with self._service_provider.provide_backend() as mdl:
38
33
  response = await mdl.invoke(mc.CompletionRequest(prompt))
39
- print(response.v.strip())
34
+
35
+ print(response.v.strip())
@@ -1,4 +1,4 @@
1
- import dataclasses as dc
1
+ from omlish import dataclasses as dc
2
2
 
3
3
  from .... import minichain as mc
4
4
 
@@ -6,7 +6,7 @@ from .... import minichain as mc
6
6
  ##
7
7
 
8
8
 
9
- DEFAULT_EMBEDDING_MODEL_BACKEND = 'openai'
9
+ DEFAULT_BACKEND = 'openai'
10
10
 
11
11
 
12
12
  ##
@@ -2,11 +2,15 @@ from omlish import dataclasses as dc
2
2
  from omlish import inject as inj
3
3
  from omlish import lang
4
4
 
5
+ from ...backends.configs import BackendConfig
6
+ from ...backends.types import DefaultBackendName
5
7
  from ..base import Session
8
+ from .configs import DEFAULT_BACKEND
6
9
  from .configs import EmbeddingConfig
7
10
 
8
11
 
9
12
  with lang.auto_proxy_import(globals()):
13
+ from ...backends import inject as _backends
10
14
  from . import session as _session
11
15
 
12
16
 
@@ -25,4 +29,14 @@ def bind_embedding(cfg: EmbeddingConfig) -> inj.Elements:
25
29
 
26
30
  #
27
31
 
32
+ els.extend([
33
+ _backends.bind_backends(BackendConfig(
34
+ backend=cfg.backend,
35
+ )),
36
+
37
+ inj.bind(DefaultBackendName, to_const=DEFAULT_BACKEND),
38
+ ])
39
+
40
+ #
41
+
28
42
  return inj.as_elements(*els)
@@ -1,11 +1,9 @@
1
- import dataclasses as dc
2
-
3
- from omlish import lang
1
+ from omlish import dataclasses as dc
4
2
  from omlish.formats import json
5
3
 
6
4
  from .... import minichain as mc
5
+ from ...backends.types import EmbeddingServiceBackendProvider
7
6
  from ..base import Session
8
- from .configs import DEFAULT_EMBEDDING_MODEL_BACKEND
9
7
  from .configs import EmbeddingConfig
10
8
 
11
9
 
@@ -21,17 +19,15 @@ class EmbeddingSession(Session['EmbeddingSession.Config']):
21
19
  self,
22
20
  config: Config,
23
21
  *,
24
- backend_catalog: mc.BackendCatalog,
22
+ service_provider: EmbeddingServiceBackendProvider,
25
23
  ) -> None:
26
24
  super().__init__(config)
27
25
 
28
- self._backend_catalog = backend_catalog
26
+ self._service_provider = service_provider
29
27
 
30
28
  async def run(self) -> None:
31
29
  mdl: mc.EmbeddingService
32
- async with lang.async_maybe_managing(self._backend_catalog.new_backend(
33
- mc.EmbeddingService,
34
- self._config.backend or DEFAULT_EMBEDDING_MODEL_BACKEND,
35
- )) as mdl:
30
+ async with self._service_provider.provide_backend() as mdl:
36
31
  response = await mdl.invoke(mc.EmbeddingRequest(self._config.content))
37
- print(json.dumps_compact(list(map(float, response.v))))
32
+
33
+ print(json.dumps_compact(list(map(float, response.v))))
@@ -1,8 +1,8 @@
1
1
  import abc
2
- import dataclasses as dc
3
2
  import os.path
4
3
  import typing as ta
5
4
 
5
+ from omlish import dataclasses as dc
6
6
  from omlish import lang
7
7
  from omlish import marshal as msh
8
8
  from omlish.formats import json
@@ -39,7 +39,7 @@ class BackendStringBackendCatalog(BackendCatalog):
39
39
 
40
40
  al: list = list(rs.args or [])
41
41
 
42
- # FIXME: lol
42
+ # FIXME: lol - move *into* local model classes as an injected dep?
43
43
  if al and isinstance(al[0], ModelRepo):
44
44
  [mr] = al
45
45
  mrr = check.not_none(self._model_repo_resolver)
File without changes
@@ -0,0 +1,75 @@
1
+ import typing as ta
2
+
3
+ from omlish import check
4
+ from omlish import marshal as msh
5
+ from omlish import typedvalues as tv
6
+ from omlish.formats import json
7
+ from omlish.http import all as http
8
+
9
+ from .....backends.groq import protocol as pt
10
+ from ....chat.choices.services import ChatChoicesRequest
11
+ from ....chat.choices.services import ChatChoicesResponse
12
+ from ....chat.choices.services import static_check_is_chat_choices_service
13
+ from ....chat.tools.types import Tool
14
+ from ....models.configs import ModelName
15
+ from ....standard import ApiKey
16
+ from ....standard import DefaultOptions
17
+ from .names import MODEL_NAMES
18
+ from .protocol import build_gq_request_messages
19
+ from .protocol import build_gq_request_tool
20
+ from .protocol import build_mc_choices_response
21
+
22
+
23
+ ##
24
+
25
+
26
+ # @omlish-manifest $.minichain.registries.manifests.RegistryManifest(
27
+ # name='groq',
28
+ # type='ChatChoicesService',
29
+ # )
30
+ @static_check_is_chat_choices_service
31
+ class GroqChatChoicesService:
32
+ DEFAULT_MODEL_NAME: ta.ClassVar[ModelName] = ModelName(check.not_none(MODEL_NAMES.default))
33
+
34
+ def __init__(
35
+ self,
36
+ *configs: ApiKey | ModelName | DefaultOptions,
37
+ http_client: http.AsyncHttpClient | None = None,
38
+ ) -> None:
39
+ super().__init__()
40
+
41
+ self._http_client = http_client
42
+
43
+ with tv.consume(*configs) as cc:
44
+ self._model_name = cc.pop(self.DEFAULT_MODEL_NAME)
45
+ self._api_key = ApiKey.pop_secret(cc, env='GROQ_API_KEY')
46
+ self._default_options: tv.TypedValues = DefaultOptions.pop(cc)
47
+
48
+ async def invoke(self, request: ChatChoicesRequest) -> ChatChoicesResponse:
49
+ tools: list[pt.ChatCompletionRequest.Tool] = []
50
+ with tv.TypedValues(*request.options).consume() as oc:
51
+ t: Tool
52
+ for t in oc.pop(Tool, []):
53
+ tools.append(build_gq_request_tool(t))
54
+
55
+ gq_request = pt.ChatCompletionRequest(
56
+ messages=build_gq_request_messages(request.v),
57
+ model=MODEL_NAMES.resolve(self._model_name.v),
58
+ tools=tools or None,
59
+ )
60
+
61
+ raw_request = msh.marshal(gq_request)
62
+
63
+ http_response = await http.async_request(
64
+ 'https://api.groq.com/openai/v1/chat/completions',
65
+ headers={
66
+ http.consts.HEADER_CONTENT_TYPE: http.consts.CONTENT_TYPE_JSON,
67
+ http.consts.HEADER_AUTH: http.consts.format_bearer_auth_header(check.not_none(self._api_key).reveal()),
68
+ },
69
+ data=json.dumps(raw_request).encode('utf-8'),
70
+ client=self._http_client,
71
+ )
72
+
73
+ raw_response = json.loads(check.not_none(http_response.data).decode('utf-8'))
74
+
75
+ return build_mc_choices_response(msh.unmarshal(raw_response, pt.ChatCompletionResponse))
@@ -0,0 +1,48 @@
1
+ """
2
+ https://console.groq.com/docs/models
3
+
4
+ curl -X GET "https://api.groq.com/openai/v1/models" \
5
+ -H "Authorization: Bearer $GROQ_API_KEY" \
6
+ -H "Content-Type: application/json"
7
+
8
+ "compound-beta",
9
+ "compound-beta-mini",
10
+ "gemma2-9b-it",
11
+ "llama-3.1-8b-instant",
12
+ "llama-3.3-70b-versatile",
13
+ "meta-llama/llama-4-maverick-17b-128e-instruct",
14
+ "meta-llama/llama-4-scout-17b-16e-instruct",
15
+ "meta-llama/llama-guard-4-12b",
16
+ "moonshotai/kimi-k2-instruct",
17
+ "openai/gpt-oss-120b",
18
+ "openai/gpt-oss-20b",
19
+ "qwen/qwen3-32b",
20
+ """
21
+ from ....models.names import ModelNameCollection
22
+ from ...strings.manifests import BackendStringsManifest
23
+
24
+
25
+ ##
26
+
27
+
28
+ MODEL_NAMES = ModelNameCollection(
29
+ default='gpt-oss-120b',
30
+ aliases={
31
+ 'gpt-oss-120b': 'openai/gpt-oss-120b',
32
+ 'openai/gpt-oss-120b': None,
33
+
34
+ 'gpt-oss-20b': 'openai/gpt-oss-20b',
35
+ 'openai/gpt-oss-20b': None,
36
+ },
37
+ )
38
+
39
+
40
+ # @omlish-manifest
41
+ _BACKEND_STRINGS_MANIFEST = BackendStringsManifest(
42
+ [
43
+ 'ChatChoicesService',
44
+ 'ChatChoicesStreamService',
45
+ ],
46
+ 'groq',
47
+ model_names=MODEL_NAMES,
48
+ )
@@ -0,0 +1,143 @@
1
+ import itertools
2
+
3
+ from omlish import check
4
+ from omlish.formats import json
5
+
6
+ from .....backends.groq import protocol as pt
7
+ from ....chat.choices.services import ChatChoicesResponse
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 AiChoiceDelta
17
+ from ....chat.stream.types import AiChoiceDeltas
18
+ from ....chat.stream.types import ContentAiChoiceDelta
19
+ from ....chat.stream.types import ToolUseAiChoiceDelta
20
+ from ....chat.tools.types import Tool
21
+ from ....content.prepare import prepare_content_str
22
+ from ....tools.jsonschema import build_tool_spec_params_json_schema
23
+ from ....tools.types import ToolUse
24
+
25
+
26
+ ##
27
+
28
+
29
+ def build_gq_request_messages(chat: Chat) -> list[pt.ChatCompletionRequest.Message]:
30
+ gq_msgs: list[pt.ChatCompletionRequest.Message] = []
31
+
32
+ for _, g in itertools.groupby(chat, lambda mc_m: isinstance(mc_m, AnyAiMessage)):
33
+ mc_msgs = list(g)
34
+
35
+ if isinstance(mc_msgs[0], AnyAiMessage):
36
+ tups: list[tuple[AiMessage | None, list[ToolUseMessage]]] = []
37
+ for mc_msg in mc_msgs:
38
+ if isinstance(mc_msg, AiMessage):
39
+ tups.append((mc_msg, []))
40
+
41
+ elif isinstance(mc_msg, ToolUseMessage):
42
+ if not tups:
43
+ tups.append((None, []))
44
+ tups[-1][1].append(mc_msg)
45
+
46
+ else:
47
+ raise TypeError(mc_msg)
48
+
49
+ for mc_ai_msg, mc_tu_msgs in tups:
50
+ gq_msgs.append(pt.ChatCompletionRequest.AssistantMessage(
51
+ content=check.isinstance(mc_ai_msg.c, str) if mc_ai_msg is not None else None,
52
+ tool_calls=[
53
+ pt.ChatCompletionRequest.AssistantMessage.ToolCall(
54
+ function=pt.ChatCompletionRequest.AssistantMessage.ToolCall.Function(
55
+ name=mc_tu_msg.tu.name,
56
+ arguments=check.not_none(mc_tu_msg.tu.raw_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
+ gq_msgs.append(pt.ChatCompletionRequest.SystemMessage(
68
+ content=check.isinstance(mc_msg.c, str),
69
+ ))
70
+
71
+ elif isinstance(mc_msg, UserMessage):
72
+ gq_msgs.append(pt.ChatCompletionRequest.UserMessage(
73
+ content=check.isinstance(mc_msg.c, str),
74
+ ))
75
+
76
+ elif isinstance(mc_msg, ToolUseResultMessage):
77
+ gq_msgs.append(pt.ChatCompletionRequest.ToolMessage(
78
+ tool_call_id=check.not_none(mc_msg.tur.id),
79
+ content=check.isinstance(mc_msg.tur.c, str),
80
+ ))
81
+
82
+ else:
83
+ raise TypeError(mc_msg)
84
+
85
+ return gq_msgs
86
+
87
+
88
+ def build_gq_request_tool(t: Tool) -> pt.ChatCompletionRequest.Tool:
89
+ return pt.ChatCompletionRequest.Tool(
90
+ function=pt.ChatCompletionRequest.Tool.Function(
91
+ name=check.not_none(t.spec.name),
92
+ description=prepare_content_str(t.spec.desc),
93
+ parameters=build_tool_spec_params_json_schema(t.spec),
94
+ ),
95
+ )
96
+
97
+
98
+ def build_mc_choices_response(gq_resp: pt.ChatCompletionResponse) -> ChatChoicesResponse:
99
+ def build_choice(gq_choice: pt.ChatCompletionResponse.Choice) -> AiChoice:
100
+ gq_msg = gq_choice.message
101
+
102
+ lst: list[AnyAiMessage] = []
103
+
104
+ if gq_msg.content is not None:
105
+ lst.append(AiMessage(
106
+ check.isinstance(gq_msg.content, str),
107
+ ))
108
+
109
+ for gq_tc in gq_msg.tool_calls or []:
110
+ lst.append(ToolUseMessage(ToolUse(
111
+ id=gq_tc.id,
112
+ name=gq_tc.function.name,
113
+ args=json.loads(gq_tc.function.arguments or '{}'),
114
+ raw_args=gq_tc.function.arguments,
115
+ )))
116
+
117
+ return AiChoice(lst)
118
+
119
+ return ChatChoicesResponse(list(map(build_choice, gq_resp.choices)))
120
+
121
+
122
+ def build_mc_ai_choice_deltas(delta: pt.ChatCompletionChunk.Choice.Delta) -> AiChoiceDeltas:
123
+ if delta.role in (None, 'assistant'):
124
+ lst: list[AiChoiceDelta] = []
125
+
126
+ if delta.content is not None:
127
+ lst.append(ContentAiChoiceDelta(delta.content))
128
+
129
+ for tc in delta.tool_calls or []:
130
+ tc_fn = check.not_none(tc.function)
131
+ lst.append(ToolUseAiChoiceDelta(
132
+ id=tc.id,
133
+ name=check.not_none(tc_fn.name),
134
+ args=json.loads(tc_fn.arguments or '{}'),
135
+ ))
136
+
137
+ return AiChoiceDeltas(lst)
138
+
139
+ elif delta.channel in ('analysis', 'commentary'):
140
+ return AiChoiceDeltas([])
141
+
142
+ else:
143
+ raise ValueError(delta)
@@ -0,0 +1,125 @@
1
+ import typing as ta
2
+
3
+ from omlish import check
4
+ from omlish import marshal as msh
5
+ from omlish import typedvalues as tv
6
+ from omlish.formats import json
7
+ from omlish.http import all as http
8
+ from omlish.http import sse
9
+ from omlish.io.buffers import DelimitingBuffer
10
+
11
+ from .....backends.groq import protocol as pt
12
+ from ....chat.choices.services import ChatChoicesOutputs
13
+ from ....chat.stream.services import ChatChoicesStreamRequest
14
+ from ....chat.stream.services import ChatChoicesStreamResponse
15
+ from ....chat.stream.services import static_check_is_chat_choices_stream_service
16
+ from ....chat.stream.types import AiChoicesDeltas
17
+ from ....chat.tools.types import Tool
18
+ from ....configs import Config
19
+ from ....resources import UseResources
20
+ from ....standard import ApiKey
21
+ from ....stream.services import StreamResponseSink
22
+ from ....stream.services import new_stream_response
23
+ from .chat import GroqChatChoicesService
24
+ from .names import MODEL_NAMES
25
+ from .protocol import build_gq_request_messages
26
+ from .protocol import build_gq_request_tool
27
+ from .protocol import build_mc_ai_choice_deltas
28
+
29
+
30
+ ##
31
+
32
+
33
+ # @omlish-manifest $.minichain.registries.manifests.RegistryManifest(
34
+ # name='groq',
35
+ # type='ChatChoicesStreamService',
36
+ # )
37
+ @static_check_is_chat_choices_stream_service
38
+ class GroqChatChoicesStreamService:
39
+ def __init__(
40
+ self,
41
+ *configs: Config,
42
+ http_client: http.AsyncHttpClient | None = None,
43
+ ) -> None:
44
+ super().__init__()
45
+
46
+ self._http_client = http_client
47
+
48
+ with tv.consume(*configs) as cc:
49
+ self._model_name = cc.pop(GroqChatChoicesService.DEFAULT_MODEL_NAME)
50
+ self._api_key = ApiKey.pop_secret(cc, env='GROQ_API_KEY')
51
+
52
+ READ_CHUNK_SIZE: ta.ClassVar[int] = -1
53
+
54
+ async def invoke(self, request: ChatChoicesStreamRequest) -> ChatChoicesStreamResponse:
55
+ tools: list[pt.ChatCompletionRequest.Tool] = []
56
+ with tv.TypedValues(*request.options).consume() as oc:
57
+ t: Tool
58
+ for t in oc.pop(Tool, []):
59
+ tools.append(build_gq_request_tool(t))
60
+
61
+ gq_request = pt.ChatCompletionRequest(
62
+ messages=build_gq_request_messages(request.v),
63
+ model=MODEL_NAMES.resolve(self._model_name.v),
64
+ tools=tools or None,
65
+ stream=True,
66
+ )
67
+
68
+ raw_request = msh.marshal(gq_request)
69
+
70
+ http_request = http.HttpRequest(
71
+ 'https://api.groq.com/openai/v1/chat/completions',
72
+ headers={
73
+ http.consts.HEADER_CONTENT_TYPE: http.consts.CONTENT_TYPE_JSON,
74
+ http.consts.HEADER_AUTH: http.consts.format_bearer_auth_header(check.not_none(self._api_key).reveal()),
75
+ },
76
+ data=json.dumps(raw_request).encode('utf-8'),
77
+ )
78
+
79
+ async with UseResources.or_new(request.options) as rs:
80
+ http_client = await rs.enter_async_context(http.manage_async_client(self._http_client))
81
+ http_response = await rs.enter_async_context(await http_client.stream_request(http_request))
82
+
83
+ async def inner(sink: StreamResponseSink[AiChoicesDeltas]) -> ta.Sequence[ChatChoicesOutputs]:
84
+ db = DelimitingBuffer([b'\r', b'\n', b'\r\n'])
85
+ sd = sse.SseDecoder()
86
+ while True:
87
+ b = await http_response.stream.read1(self.READ_CHUNK_SIZE)
88
+ for l in db.feed(b):
89
+ if isinstance(l, DelimitingBuffer.Incomplete):
90
+ # FIXME: handle
91
+ return []
92
+
93
+ # FIXME: https://platform.openai.com/docs/guides/function-calling?api-mode=responses#streaming
94
+ for so in sd.process_line(l):
95
+ if isinstance(so, sse.SseEvent) and so.type == b'message':
96
+ ss = so.data.decode('utf-8')
97
+ if ss == '[DONE]':
98
+ return []
99
+
100
+ sj = json.loads(ss) # ChatCompletionChunk
101
+
102
+ check.state(sj['object'] == 'chat.completion.chunk')
103
+
104
+ ccc = msh.unmarshal(sj, pt.ChatCompletionChunk)
105
+
106
+ # FIXME: stop reason
107
+ if not ccc.choices:
108
+ continue
109
+
110
+ if any(choice.finish_reason for choice in ccc.choices):
111
+ check.state(all(choice.finish_reason for choice in ccc.choices))
112
+ break
113
+
114
+ await sink.emit(AiChoicesDeltas([
115
+ build_mc_ai_choice_deltas(choice.delta)
116
+ for choice in ccc.choices
117
+ ]))
118
+
119
+ if not b:
120
+ return []
121
+
122
+ # raw_response = json.loads(check.not_none(http_response.data).decode('utf-8'))
123
+ # return rh.build_response(raw_response)
124
+
125
+ return await new_stream_response(rs, inner)