ommlds 0.0.0.dev465__py3-none-any.whl → 0.0.0.dev467__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.

Potentially problematic release.


This version of ommlds might be problematic. Click here for more details.

@@ -2,23 +2,24 @@ import typing as ta
2
2
 
3
3
  from omlish import cached
4
4
  from omlish import check
5
- from omlish import lang
6
5
  from omlish import typedvalues as tv
7
6
  from omlish.formats import json
8
7
 
8
+ from .....backends.openai import protocol as pt
9
9
  from ....chat.choices.services import ChatChoicesResponse
10
10
  from ....chat.choices.types import AiChoice
11
+ from ....chat.choices.types import AiChoices
11
12
  from ....chat.choices.types import ChatChoicesOptions
12
- from ....chat.messages import AiChat
13
13
  from ....chat.messages import AiMessage
14
14
  from ....chat.messages import AnyAiMessage
15
15
  from ....chat.messages import Chat
16
- from ....chat.messages import Message
17
16
  from ....chat.messages import SystemMessage
18
17
  from ....chat.messages import ToolUseMessage
19
18
  from ....chat.messages import ToolUseResultMessage
20
19
  from ....chat.messages import UserMessage
20
+ from ....chat.stream.types import AiChoiceDelta
21
21
  from ....chat.stream.types import ContentAiChoiceDelta
22
+ from ....chat.stream.types import PartialToolUseAiChoiceDelta
22
23
  from ....chat.tools.types import Tool
23
24
  from ....content.json import JsonContent
24
25
  from ....content.prepare import prepare_content_str
@@ -26,7 +27,7 @@ from ....llms.types import MaxTokens
26
27
  from ....llms.types import Temperature
27
28
  from ....llms.types import TokenUsage
28
29
  from ....llms.types import TokenUsageOutput
29
- from ....tools.jsonschema import build_tool_spec_json_schema
30
+ from ....tools.jsonschema import build_tool_spec_params_json_schema
30
31
  from ....tools.types import ToolSpec
31
32
  from ....tools.types import ToolUse
32
33
  from ....types import Option
@@ -35,61 +36,115 @@ from ....types import Option
35
36
  ##
36
37
 
37
38
 
38
- def build_request_messages(chat: Chat) -> ta.Sequence[ta.Mapping[str, ta.Any]]:
39
- out: list[dict[str, ta.Any]] = []
39
+ def build_oai_request_msgs(mc_chat: Chat) -> ta.Sequence[pt.ChatCompletionMessage]:
40
+ oai_msgs: list[pt.ChatCompletionMessage] = []
40
41
 
41
- for m in chat:
42
- if isinstance(m, SystemMessage):
43
- out.append(dict(
44
- role='system',
45
- content=m.c,
42
+ for mc_msg in mc_chat:
43
+ if isinstance(mc_msg, SystemMessage):
44
+ oai_msgs.append(pt.SystemChatCompletionMessage(
45
+ content=check.isinstance(mc_msg.c, str),
46
46
  ))
47
47
 
48
- elif isinstance(m, AiMessage):
49
- out.append(dict(
50
- role='assistant',
51
- content=check.isinstance(m.c, (str, None)),
48
+ elif isinstance(mc_msg, AiMessage):
49
+ oai_msgs.append(pt.AssistantChatCompletionMessage(
50
+ content=check.isinstance(mc_msg.c, (str, None)),
52
51
  ))
53
52
 
54
- elif isinstance(m, ToolUseMessage):
55
- out.append(dict(
56
- role='assistant',
57
- tool_calls=[
58
- dict(
59
- id=m.tu.id,
60
- function=dict(
61
- arguments=check.not_none(m.tu.raw_args),
62
- name=m.tu.name,
63
- ),
64
- type='function',
53
+ elif isinstance(mc_msg, ToolUseMessage):
54
+ oai_msgs.append(pt.AssistantChatCompletionMessage(
55
+ tool_calls=[pt.AssistantChatCompletionMessage.ToolCall(
56
+ id=check.not_none(mc_msg.tu.id),
57
+ function=pt.AssistantChatCompletionMessage.ToolCall.Function(
58
+ arguments=check.not_none(mc_msg.tu.raw_args),
59
+ name=mc_msg.tu.name,
65
60
  ),
66
- ],
61
+ )],
67
62
  ))
68
63
 
69
- elif isinstance(m, UserMessage):
70
- out.append(dict(
71
- role='user',
72
- content=prepare_content_str(m.c),
64
+ elif isinstance(mc_msg, UserMessage):
65
+ oai_msgs.append(pt.UserChatCompletionMessage(
66
+ content=prepare_content_str(mc_msg.c),
73
67
  ))
74
68
 
75
- elif isinstance(m, ToolUseResultMessage):
69
+ elif isinstance(mc_msg, ToolUseResultMessage):
76
70
  tc: str
77
- if isinstance(m.tur.c, str):
78
- tc = m.tur.c
79
- elif isinstance(m.tur.c, JsonContent):
80
- tc = json.dumps_compact(m.tur.c)
71
+ if isinstance(mc_msg.tur.c, str):
72
+ tc = mc_msg.tur.c
73
+ elif isinstance(mc_msg.tur.c, JsonContent):
74
+ tc = json.dumps_compact(mc_msg.tur.c)
81
75
  else:
82
- raise TypeError(m.tur.c)
83
- out.append(dict(
84
- role='tool',
85
- tool_call_id=m.tur.id,
76
+ raise TypeError(mc_msg.tur.c)
77
+ oai_msgs.append(pt.ToolChatCompletionMessage(
78
+ tool_call_id=check.not_none(mc_msg.tur.id),
86
79
  content=tc,
87
80
  ))
88
81
 
89
82
  else:
90
- raise TypeError(m)
83
+ raise TypeError(mc_msg)
91
84
 
92
- return out
85
+ return oai_msgs
86
+
87
+
88
+ #
89
+
90
+
91
+ def build_mc_ai_choice(oai_choice: pt.ChatCompletionResponseChoice) -> AiChoice:
92
+ cur: list[AnyAiMessage] = []
93
+
94
+ oai_msg = oai_choice.message
95
+
96
+ if (oai_c := oai_msg.content) is not None:
97
+ cur.append(AiMessage(check.isinstance(oai_c, str)))
98
+
99
+ for oai_tc in oai_msg.tool_calls or []:
100
+ cur.append(ToolUseMessage(ToolUse(
101
+ id=oai_tc.id,
102
+ name=oai_tc.function.name,
103
+ args=json.loads(oai_tc.function.arguments or '{}'),
104
+ raw_args=oai_tc.function.arguments,
105
+ )))
106
+
107
+ return AiChoice(cur)
108
+
109
+
110
+ def build_mc_ai_choices(oai_resp: pt.ChatCompletionResponse) -> AiChoices:
111
+ return [
112
+ build_mc_ai_choice(oai_choice)
113
+ for oai_choice in oai_resp.choices
114
+ ]
115
+
116
+
117
+ def build_mc_choices_response(oai_resp: pt.ChatCompletionResponse) -> ChatChoicesResponse:
118
+ return ChatChoicesResponse(
119
+ build_mc_ai_choices(oai_resp),
120
+
121
+ tv.TypedValues(
122
+ *([TokenUsageOutput(TokenUsage(
123
+ input=tu.prompt_tokens,
124
+ output=tu.completion_tokens,
125
+ total=tu.total_tokens,
126
+ ))] if (tu := oai_resp.usage) is not None else []),
127
+ ),
128
+ )
129
+
130
+
131
+ def build_mc_ai_choice_delta(delta: pt.ChatCompletionChunkChoiceDelta) -> AiChoiceDelta:
132
+ if delta.content is not None:
133
+ check.state(not delta.tool_calls)
134
+ return ContentAiChoiceDelta(delta.content)
135
+
136
+ elif delta.tool_calls is not None:
137
+ check.state(delta.content is None)
138
+ tc = check.single(delta.tool_calls)
139
+ tc_fn = check.not_none(tc.function)
140
+ return PartialToolUseAiChoiceDelta(
141
+ id=tc.id,
142
+ name=tc_fn.name,
143
+ raw_args=tc_fn.arguments,
144
+ )
145
+
146
+ else:
147
+ raise ValueError(delta)
93
148
 
94
149
 
95
150
  ##
@@ -110,14 +165,6 @@ class OpenaiChatRequestHandler:
110
165
  self._model = model
111
166
  self._mandatory_kwargs = mandatory_kwargs
112
167
 
113
- ROLES_MAP: ta.ClassVar[ta.Mapping[type[Message], str]] = {
114
- SystemMessage: 'system',
115
- UserMessage: 'user',
116
- AiMessage: 'assistant',
117
- ToolUseMessage: 'assistant',
118
- ToolUseResultMessage: 'tool',
119
- }
120
-
121
168
  DEFAULT_OPTIONS: ta.ClassVar[tv.TypedValues[Option]] = tv.TypedValues[Option](
122
169
  Temperature(0.),
123
170
  MaxTokens(1024),
@@ -160,69 +207,26 @@ class OpenaiChatRequestHandler:
160
207
  )
161
208
 
162
209
  @cached.function
163
- def raw_request(self) -> ta.Mapping[str, ta.Any]:
210
+ def oai_request(self) -> pt.ChatCompletionRequest:
164
211
  po = self._process_options()
165
212
 
166
- tools = [
167
- dict(
168
- type='function',
169
- function=build_tool_spec_json_schema(ts),
213
+ tools: list[pt.ChatCompletionRequestTool] = [
214
+ pt.ChatCompletionRequestTool(
215
+ function=pt.ChatCompletionRequestTool.Function(
216
+ name=check.not_none(ts.name),
217
+ description=prepare_content_str(ts.desc),
218
+ parameters=build_tool_spec_params_json_schema(ts),
219
+ ),
170
220
  )
171
221
  for ts in po.tools_by_name.values()
172
222
  ]
173
223
 
174
- return dict(
224
+ return pt.ChatCompletionRequest(
175
225
  model=self._model,
176
- messages=build_request_messages(self._chat),
226
+ messages=build_oai_request_msgs(self._chat),
177
227
  top_p=1,
178
- **lang.opt_kw(tools=tools),
228
+ tools=tools or None,
179
229
  frequency_penalty=0.0,
180
230
  presence_penalty=0.0,
181
231
  **po.kwargs,
182
232
  )
183
-
184
- def build_ai_chat(self, message: ta.Mapping[str, ta.Any]) -> AiChat:
185
- out: list[AnyAiMessage] = []
186
- if (c := message.get('content')) is not None:
187
- out.append(AiMessage(c))
188
- for tc in message.get('tool_calls', []):
189
- out.append(ToolUseMessage(
190
- ToolUse(
191
- id=tc['id'],
192
- name=tc['function']['name'],
193
- args=json.loads(tc['function']['arguments'] or '{}'),
194
- raw_args=tc['function']['arguments'],
195
- ),
196
- ))
197
- return out
198
-
199
- def build_response(self, raw_response: ta.Mapping[str, ta.Any]) -> ChatChoicesResponse:
200
- return ChatChoicesResponse(
201
- [
202
- AiChoice(self.build_ai_chat(choice['message']))
203
- for choice in raw_response['choices']
204
- ],
205
-
206
- tv.TypedValues(
207
- *([TokenUsageOutput(TokenUsage(
208
- input=tu['prompt_tokens'],
209
- output=tu['completion_tokens'],
210
- total=tu['total_tokens'],
211
- ))] if (tu := raw_response.get('usage')) is not None else []),
212
- ),
213
- )
214
-
215
- def build_ai_choice_delta(self, delta: ta.Mapping[str, ta.Any]) -> ContentAiChoiceDelta:
216
- return ContentAiChoiceDelta(
217
- check.not_none(delta.get('content')),
218
- # FIXME:
219
- # tool_exec_requests=[
220
- # ToolUse(
221
- # id=tc['id'],
222
- # spec=self._process_options().tools_by_name[tc['function']['name']],
223
- # args=json.loads(tc['function']['arguments'] or '{}'),
224
- # raw_args=tc['function']['arguments'],
225
- # )
226
- # for tc in message_or_delta.get('tool_calls', [])
227
- # ] or None,
228
- )
@@ -11,7 +11,7 @@ from omlish.http import all as http
11
11
  from omlish.http import sse
12
12
  from omlish.io.buffers import DelimitingBuffer
13
13
 
14
- from .....backends.openai.protocol.chatcompletion.chunk import ChatCompletionChunk
14
+ from .....backends.openai import protocol as pt
15
15
  from ....chat.choices.services import ChatChoicesOutputs
16
16
  from ....chat.stream.services import ChatChoicesStreamRequest
17
17
  from ....chat.stream.services import ChatChoicesStreamResponse
@@ -28,6 +28,7 @@ from ....stream.services import StreamResponseSink
28
28
  from ....stream.services import new_stream_response
29
29
  from .chat import OpenaiChatChoicesService
30
30
  from .format import OpenaiChatRequestHandler
31
+ from .format import build_mc_ai_choice_delta
31
32
  from .names import MODEL_NAMES
32
33
 
33
34
 
@@ -62,13 +63,13 @@ class OpenaiChatChoicesStreamService:
62
63
  model=MODEL_NAMES.resolve(self._model_name.v),
63
64
  mandatory_kwargs=dict(
64
65
  stream=True,
65
- stream_options=dict(
66
+ stream_options=pt.ChatCompletionRequest.StreamOptions(
66
67
  include_usage=True,
67
68
  ),
68
69
  ),
69
70
  )
70
71
 
71
- raw_request = rh.raw_request()
72
+ raw_request = msh.marshal(rh.oai_request())
72
73
 
73
74
  http_request = http.HttpRequest(
74
75
  'https://api.openai.com/v1/chat/completions',
@@ -105,20 +106,20 @@ class OpenaiChatChoicesStreamService:
105
106
 
106
107
  check.state(sj['object'] == 'chat.completion.chunk')
107
108
 
108
- ccc = msh.unmarshal(sj, ChatCompletionChunk) # noqa
109
- # print(ccc)
109
+ ccc = msh.unmarshal(sj, pt.ChatCompletionChunk)
110
110
 
111
111
  # FIXME: stop reason
112
- if not sj['choices']:
112
+ if not ccc.choices:
113
113
  continue
114
114
 
115
- if any(choice['delta'] for choice in sj['choices']):
116
- await sink.emit(AiChoicesDeltas([
117
- AiChoiceDeltas(
118
- [rh.build_ai_choice_delta(choice['delta'])] if choice['delta'] else [],
119
- )
120
- for choice in sj['choices']
121
- ]))
115
+ if any(choice.finish_reason for choice in ccc.choices):
116
+ check.state(all(choice.finish_reason for choice in ccc.choices))
117
+ break
118
+
119
+ await sink.emit(AiChoicesDeltas([
120
+ AiChoiceDeltas([build_mc_ai_choice_delta(choice.delta)])
121
+ for choice in ccc.choices
122
+ ]))
122
123
 
123
124
  if not b:
124
125
  return []
@@ -1,22 +1,14 @@
1
- from omlish import check
2
1
  from omlish import dataclasses as dc
3
- from omlish import lang
4
2
 
5
3
  from ...services import Response
6
- from ...tools.types import ToolUse
7
4
  from ..choices.services import ChatChoicesRequest
8
5
  from ..choices.services import static_check_is_chat_choices_service
9
6
  from ..choices.types import AiChoice
10
7
  from ..choices.types import AiChoices
11
- from ..messages import AiMessage
12
- from ..messages import AnyAiMessage
13
- from ..messages import ToolUseMessage
8
+ from .joining import AiChoiceDeltaJoiner
14
9
  from .services import ChatChoicesOutputs
15
10
  from .services import ChatChoicesStreamOutputs
16
11
  from .services import ChatChoicesStreamService
17
- from .types import AiChoiceDelta
18
- from .types import ContentAiChoiceDelta
19
- from .types import ToolUseAiChoiceDelta
20
12
 
21
13
 
22
14
  ##
@@ -31,50 +23,13 @@ class ChatChoicesStreamServiceChatChoicesService:
31
23
  AiChoices,
32
24
  ChatChoicesOutputs | ChatChoicesStreamOutputs,
33
25
  ]:
34
- choice_lsts: list[list[list[str] | ToolUse]] = []
35
-
36
- def add(l: list[list[str] | ToolUse], d: AiChoiceDelta) -> None:
37
- if isinstance(d, ContentAiChoiceDelta):
38
- s = check.isinstance(d.c, str)
39
- if l and isinstance(l[-1], list):
40
- l[-1].append(s)
41
- else:
42
- l.append([s])
43
-
44
- elif isinstance(d, ToolUseAiChoiceDelta):
45
- l.append(d.tu)
46
-
47
- else:
48
- raise TypeError(d)
26
+ joiner = AiChoiceDeltaJoiner()
49
27
 
50
28
  async with (resp := await self.service.invoke(request)).v as it: # noqa
51
- i = -1 # noqa
52
- l: list[list[str] | ToolUse]
53
- async for i, cs in lang.async_enumerate(it):
54
- if i == 0:
55
- for c in cs.choices:
56
- choice_lsts.append(l := [])
57
- for d in c.deltas:
58
- add(l, d)
59
-
60
- else:
61
- for l, c in zip(choice_lsts, cs.choices, strict=True):
62
- for d in c.deltas:
63
- add(l, d)
29
+ async for cs in it:
30
+ joiner.add(cs.choices)
64
31
 
65
32
  # check.state(resp_v.is_done)
66
33
 
67
- ret: list[AiChoice] = []
68
- for cl in choice_lsts:
69
- cc: list[AnyAiMessage] = []
70
- for e in cl:
71
- if isinstance(e, list):
72
- cc.append(AiMessage(''.join(e)))
73
- elif isinstance(e, ToolUse):
74
- cc.append(ToolUseMessage(e))
75
- else:
76
- raise TypeError(e)
77
- ret.append(AiChoice(cc))
78
-
79
34
  # FIXME: outputs lol
80
- return Response(ret)
35
+ return Response([AiChoice(ms) for ms in joiner.build()])
@@ -0,0 +1,96 @@
1
+ import typing as ta
2
+
3
+ from omlish import check
4
+ from omlish.formats import json
5
+
6
+ from ...tools.types import ToolUse
7
+ from ..messages import AiChat
8
+ from ..messages import AiMessage
9
+ from ..messages import AnyAiMessage
10
+ from ..messages import ToolUseMessage
11
+ from .types import AiChoiceDelta
12
+ from .types import AiChoiceDeltas
13
+ from .types import ContentAiChoiceDelta
14
+ from .types import PartialToolUseAiChoiceDelta
15
+ from .types import ToolUseAiChoiceDelta
16
+
17
+
18
+ ##
19
+
20
+
21
+ class AiChoiceDeltaJoiner:
22
+ def __init__(self) -> None:
23
+ super().__init__()
24
+
25
+ self._seq = 0
26
+ self._channels: list[AiChoiceDeltaJoiner._Channel] = []
27
+
28
+ class _Channel(ta.NamedTuple):
29
+ deltas: list[AiChoiceDelta]
30
+ messages: list[AnyAiMessage]
31
+
32
+ def _build_joined(self, deltas: ta.Sequence[AiChoiceDelta]) -> AnyAiMessage:
33
+ dty = check.single(set(map(type, check.not_empty(deltas))))
34
+
35
+ if dty is ContentAiChoiceDelta:
36
+ cds = ta.cast(ta.Sequence[ContentAiChoiceDelta], deltas)
37
+ return AiMessage(''.join(check.isinstance(cd.c, str) for cd in cds))
38
+
39
+ elif dty is ToolUseAiChoiceDelta:
40
+ raise TypeError(dty)
41
+
42
+ elif dty is PartialToolUseAiChoiceDelta:
43
+ tds = ta.cast(ta.Sequence[PartialToolUseAiChoiceDelta], deltas)
44
+ for td in ta.cast(ta.Sequence[PartialToolUseAiChoiceDelta], deltas)[1:]:
45
+ check.none(td.id)
46
+ check.none(td.name)
47
+
48
+ ra = ''.join(filter(None, (td.raw_args for td in tds)))
49
+
50
+ return ToolUseMessage(ToolUse(
51
+ id=tds[0].id,
52
+ name=check.non_empty_str(tds[0].name),
53
+ args=json.loads(ra),
54
+ raw_args=ra,
55
+ ))
56
+
57
+ else:
58
+ raise TypeError(dty)
59
+
60
+ def _join_one(self, chan: _Channel) -> None:
61
+ if not chan.deltas:
62
+ return
63
+
64
+ chan.messages.append(self._build_joined(chan.deltas))
65
+ chan.deltas.clear()
66
+
67
+ def _add_to(self, chan: _Channel, d: AiChoiceDelta) -> None:
68
+ if chan.deltas and type(chan.deltas[0]) is not type(d):
69
+ self._join_one(chan)
70
+
71
+ if isinstance(d, ToolUseAiChoiceDelta):
72
+ chan.messages.append(ToolUseMessage(ToolUse(
73
+ id=d.id,
74
+ name=check.not_none(d.name),
75
+ args=d.args or {},
76
+ )))
77
+
78
+ else:
79
+ chan.deltas.append(d)
80
+
81
+ def add(self, choices: ta.Sequence[AiChoiceDeltas]) -> None:
82
+ if not self._seq:
83
+ check.empty(self._channels)
84
+ self._channels.extend(self._Channel([], []) for _ in range(len(choices)))
85
+
86
+ for chan, c in zip(self._channels, choices, strict=True):
87
+ for d in c.deltas:
88
+ self._add_to(chan, d)
89
+
90
+ self._seq += 1
91
+
92
+ def build(self) -> list[AiChat]:
93
+ for chan in self._channels:
94
+ self._join_one(chan)
95
+
96
+ return [list(chan.messages) for chan in self._channels]
@@ -6,7 +6,6 @@ from omlish import marshal as msh
6
6
 
7
7
  from ...content.types import Content
8
8
  from ...stream.services import StreamOptions
9
- from ...tools.types import ToolUse
10
9
  from ...types import Option
11
10
  from ...types import Output
12
11
  from ..choices.types import ChatChoicesOptions
@@ -43,14 +42,31 @@ class AiChoiceDelta(lang.Sealed, lang.Abstract):
43
42
  pass
44
43
 
45
44
 
45
+ #
46
+
47
+
46
48
  @dc.dataclass(frozen=True)
47
49
  class ContentAiChoiceDelta(AiChoiceDelta, lang.Final):
48
50
  c: Content
49
51
 
50
52
 
51
- @dc.dataclass(frozen=True)
52
- class ToolUseAiChoiceDelta(AiChoiceDelta, lang.Final):
53
- tu: ToolUse
53
+ #
54
+
55
+
56
+ @dc.dataclass(frozen=True, kw_only=True)
57
+ class AnyToolUseAiChoiceDelta(AiChoiceDelta, lang.Abstract):
58
+ id: str | None = None
59
+ name: str | None = None
60
+
61
+
62
+ @dc.dataclass(frozen=True, kw_only=True)
63
+ class ToolUseAiChoiceDelta(AnyToolUseAiChoiceDelta, lang.Final):
64
+ args: ta.Mapping[str, ta.Any] | None = None
65
+
66
+
67
+ @dc.dataclass(frozen=True, kw_only=True)
68
+ class PartialToolUseAiChoiceDelta(AnyToolUseAiChoiceDelta, lang.Final):
69
+ raw_args: ta.Any | None = None
54
70
 
55
71
 
56
72
  #
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ommlds
3
- Version: 0.0.0.dev465
3
+ Version: 0.0.0.dev467
4
4
  Summary: ommlds
5
5
  Author: wrmsr
6
6
  License-Expression: BSD-3-Clause
@@ -14,8 +14,8 @@ 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: omdev==0.0.0.dev465
18
- Requires-Dist: omlish==0.0.0.dev465
17
+ Requires-Dist: omdev==0.0.0.dev467
18
+ Requires-Dist: omlish==0.0.0.dev467
19
19
  Provides-Extra: all
20
20
  Requires-Dist: llama-cpp-python~=0.3; extra == "all"
21
21
  Requires-Dist: mlx~=0.29; extra == "all"