ommlds 0.0.0.dev503__py3-none-any.whl → 0.0.0.dev505__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 +5 -5
- ommlds/__about__.py +1 -1
- ommlds/backends/anthropic/protocol/_dataclasses.py +16 -16
- ommlds/backends/cerebras/_dataclasses.py +42 -42
- ommlds/backends/google/protocol/_dataclasses.py +64 -64
- ommlds/backends/groq/_dataclasses.py +36 -36
- ommlds/backends/ollama/_dataclasses.py +28 -28
- ommlds/backends/openai/protocol/_dataclasses.py +88 -88
- ommlds/backends/tavily/_dataclasses.py +16 -16
- ommlds/cli/_dataclasses.py +212 -43
- ommlds/cli/sessions/chat/interfaces/textual/app.py +34 -0
- ommlds/cli/sessions/chat/interfaces/textual/configs.py +1 -1
- ommlds/cli/sessions/chat/interfaces/textual/inject.py +14 -0
- ommlds/cli/sessions/chat/interfaces/textual/inputhistory.py +174 -0
- ommlds/cli/sessions/chat/interfaces/textual/widgets/input.py +42 -8
- ommlds/minichain/_dataclasses.py +361 -343
- ommlds/minichain/backends/impls/cerebras/stream.py +39 -52
- ommlds/minichain/backends/impls/google/chat.py +11 -82
- ommlds/minichain/backends/impls/google/protocol.py +105 -0
- ommlds/minichain/backends/impls/google/stream.py +49 -132
- ommlds/minichain/backends/impls/groq/stream.py +40 -53
- ommlds/minichain/backends/impls/openai/stream.py +40 -87
- ommlds/minichain/http/__init__.py +0 -0
- ommlds/minichain/http/stream.py +195 -0
- {ommlds-0.0.0.dev503.dist-info → ommlds-0.0.0.dev505.dist-info}/METADATA +6 -6
- {ommlds-0.0.0.dev503.dist-info → ommlds-0.0.0.dev505.dist-info}/RECORD +30 -26
- {ommlds-0.0.0.dev503.dist-info → ommlds-0.0.0.dev505.dist-info}/WHEEL +0 -0
- {ommlds-0.0.0.dev503.dist-info → ommlds-0.0.0.dev505.dist-info}/entry_points.txt +0 -0
- {ommlds-0.0.0.dev503.dist-info → ommlds-0.0.0.dev505.dist-info}/licenses/LICENSE +0 -0
- {ommlds-0.0.0.dev503.dist-info → ommlds-0.0.0.dev505.dist-info}/top_level.txt +0 -0
|
@@ -6,20 +6,17 @@ from omlish import typedvalues as tv
|
|
|
6
6
|
from omlish.formats import json
|
|
7
7
|
from omlish.http import all as http
|
|
8
8
|
from omlish.http import sse
|
|
9
|
-
from omlish.io.buffers import DelimitingBuffer
|
|
10
9
|
|
|
11
10
|
from .....backends.cerebras import protocol as pt
|
|
12
|
-
from ....chat.choices.services import ChatChoicesOutputs
|
|
13
11
|
from ....chat.choices.stream.services import ChatChoicesStreamRequest
|
|
14
12
|
from ....chat.choices.stream.services import ChatChoicesStreamResponse
|
|
15
13
|
from ....chat.choices.stream.services import static_check_is_chat_choices_stream_service
|
|
16
14
|
from ....chat.choices.stream.types import AiChoicesDeltas
|
|
17
15
|
from ....chat.tools.types import Tool
|
|
18
16
|
from ....configs import Config
|
|
19
|
-
from ....
|
|
17
|
+
from ....http.stream import BytesHttpStreamResponseBuilder
|
|
18
|
+
from ....http.stream import SimpleSseLinesHttpStreamResponseHandler
|
|
20
19
|
from ....standard import ApiKey
|
|
21
|
-
from ....stream.services import StreamResponseSink
|
|
22
|
-
from ....stream.services import new_stream_response
|
|
23
20
|
from .chat import CerebrasChatChoicesService
|
|
24
21
|
from .names import MODEL_NAMES
|
|
25
22
|
from .protocol import build_cer_request_messages
|
|
@@ -49,6 +46,35 @@ class CerebrasChatChoicesStreamService:
|
|
|
49
46
|
self._model_name = cc.pop(CerebrasChatChoicesService.DEFAULT_MODEL_NAME)
|
|
50
47
|
self._api_key = ApiKey.pop_secret(cc, env='CEREBRAS_API_KEY')
|
|
51
48
|
|
|
49
|
+
URL: ta.ClassVar[str] = 'https://api.cerebras.ai/v1/chat/completions'
|
|
50
|
+
|
|
51
|
+
def _process_sse(self, so: sse.SseDecoderOutput) -> ta.Sequence[AiChoicesDeltas | None]:
|
|
52
|
+
if not (isinstance(so, sse.SseEvent) and so.type == b'message'):
|
|
53
|
+
return []
|
|
54
|
+
|
|
55
|
+
ss = so.data.decode('utf-8')
|
|
56
|
+
if ss == '[DONE]':
|
|
57
|
+
return [None]
|
|
58
|
+
|
|
59
|
+
sj = json.loads(ss) # ChatCompletionChunk
|
|
60
|
+
|
|
61
|
+
check.state(sj['object'] == 'chat.completion.chunk')
|
|
62
|
+
|
|
63
|
+
ccc = msh.unmarshal(sj, pt.ChatCompletionChunk)
|
|
64
|
+
|
|
65
|
+
# FIXME: stop reason
|
|
66
|
+
if not ccc.choices:
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
if any(choice.finish_reason for choice in ccc.choices):
|
|
70
|
+
check.state(all(choice.finish_reason for choice in ccc.choices))
|
|
71
|
+
return [None]
|
|
72
|
+
|
|
73
|
+
return [AiChoicesDeltas([
|
|
74
|
+
build_mc_ai_choice_deltas(choice.delta)
|
|
75
|
+
for choice in ccc.choices
|
|
76
|
+
])]
|
|
77
|
+
|
|
52
78
|
READ_CHUNK_SIZE: ta.ClassVar[int] = -1
|
|
53
79
|
|
|
54
80
|
async def invoke(self, request: ChatChoicesStreamRequest) -> ChatChoicesStreamResponse:
|
|
@@ -76,50 +102,11 @@ class CerebrasChatChoicesStreamService:
|
|
|
76
102
|
data=json.dumps(raw_request).encode('utf-8'),
|
|
77
103
|
)
|
|
78
104
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
http_response
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
raise TypeError(l)
|
|
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)
|
|
105
|
+
return await BytesHttpStreamResponseBuilder(
|
|
106
|
+
self._http_client,
|
|
107
|
+
lambda http_response: SimpleSseLinesHttpStreamResponseHandler(self._process_sse).as_lines().as_bytes(),
|
|
108
|
+
read_chunk_size=self.READ_CHUNK_SIZE,
|
|
109
|
+
).new_stream_response(
|
|
110
|
+
http_request,
|
|
111
|
+
request.options,
|
|
112
|
+
)
|
|
@@ -16,16 +16,14 @@ from ....chat.choices.services import static_check_is_chat_choices_service
|
|
|
16
16
|
from ....chat.choices.types import AiChoice
|
|
17
17
|
from ....chat.messages import AiMessage
|
|
18
18
|
from ....chat.messages import AnyAiMessage
|
|
19
|
-
from ....chat.messages import Message
|
|
20
|
-
from ....chat.messages import SystemMessage
|
|
21
19
|
from ....chat.messages import ToolUseMessage
|
|
22
|
-
from ....chat.messages import ToolUseResultMessage
|
|
23
|
-
from ....chat.messages import UserMessage
|
|
24
20
|
from ....chat.tools.types import Tool
|
|
25
21
|
from ....models.configs import ModelName
|
|
26
22
|
from ....standard import ApiKey
|
|
27
23
|
from ....tools.types import ToolUse
|
|
28
24
|
from .names import MODEL_NAMES
|
|
25
|
+
from .protocol import make_msg_content
|
|
26
|
+
from .protocol import pop_system_instructions
|
|
29
27
|
from .tools import build_tool_spec_schema
|
|
30
28
|
|
|
31
29
|
|
|
@@ -53,90 +51,14 @@ class GoogleChatChoicesService:
|
|
|
53
51
|
self._model_name = cc.pop(self.DEFAULT_MODEL_NAME)
|
|
54
52
|
self._api_key = ApiKey.pop_secret(cc, env='GEMINI_API_KEY')
|
|
55
53
|
|
|
56
|
-
def _get_msg_content(self, m: Message) -> str | None:
|
|
57
|
-
if isinstance(m, AiMessage):
|
|
58
|
-
return check.isinstance(m.c, str)
|
|
59
|
-
|
|
60
|
-
elif isinstance(m, (SystemMessage, UserMessage)):
|
|
61
|
-
return check.isinstance(m.c, str)
|
|
62
|
-
|
|
63
|
-
else:
|
|
64
|
-
raise TypeError(m)
|
|
65
|
-
|
|
66
54
|
BASE_URL: ta.ClassVar[str] = 'https://generativelanguage.googleapis.com/v1beta/models'
|
|
67
55
|
|
|
68
|
-
ROLES_MAP: ta.ClassVar[ta.Mapping[type[Message], str]] = {
|
|
69
|
-
UserMessage: 'user',
|
|
70
|
-
AiMessage: 'model',
|
|
71
|
-
ToolUseMessage: 'model',
|
|
72
|
-
}
|
|
73
|
-
|
|
74
56
|
async def invoke(
|
|
75
57
|
self,
|
|
76
58
|
request: ChatChoicesRequest,
|
|
77
59
|
) -> ChatChoicesResponse:
|
|
78
60
|
key = check.not_none(self._api_key).reveal()
|
|
79
61
|
|
|
80
|
-
g_sys_content: pt.Content | None = None
|
|
81
|
-
g_contents: list[pt.Content] = []
|
|
82
|
-
for i, m in enumerate(request.v):
|
|
83
|
-
if isinstance(m, SystemMessage):
|
|
84
|
-
check.arg(i == 0)
|
|
85
|
-
check.none(g_sys_content)
|
|
86
|
-
g_sys_content = pt.Content(
|
|
87
|
-
parts=[pt.Part(
|
|
88
|
-
text=check.not_none(self._get_msg_content(m)),
|
|
89
|
-
)],
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
elif isinstance(m, ToolUseResultMessage):
|
|
93
|
-
tr_resp_val: pt.Value
|
|
94
|
-
if m.tur.c is None:
|
|
95
|
-
tr_resp_val = pt.NullValue() # type: ignore[unreachable]
|
|
96
|
-
elif isinstance(m.tur.c, str):
|
|
97
|
-
tr_resp_val = pt.StringValue(m.tur.c)
|
|
98
|
-
else:
|
|
99
|
-
raise TypeError(m.tur.c)
|
|
100
|
-
g_contents.append(pt.Content(
|
|
101
|
-
parts=[pt.Part(
|
|
102
|
-
function_response=pt.FunctionResponse(
|
|
103
|
-
id=m.tur.id,
|
|
104
|
-
name=m.tur.name,
|
|
105
|
-
response={
|
|
106
|
-
'value': tr_resp_val,
|
|
107
|
-
},
|
|
108
|
-
),
|
|
109
|
-
)],
|
|
110
|
-
))
|
|
111
|
-
|
|
112
|
-
elif isinstance(m, AiMessage):
|
|
113
|
-
g_contents.append(pt.Content(
|
|
114
|
-
parts=[pt.Part(
|
|
115
|
-
text=check.not_none(self._get_msg_content(m)),
|
|
116
|
-
)],
|
|
117
|
-
role='model',
|
|
118
|
-
))
|
|
119
|
-
|
|
120
|
-
elif isinstance(m, ToolUseMessage):
|
|
121
|
-
g_contents.append(pt.Content(
|
|
122
|
-
parts=[pt.Part(
|
|
123
|
-
function_call=pt.FunctionCall(
|
|
124
|
-
id=m.tu.id,
|
|
125
|
-
name=m.tu.name,
|
|
126
|
-
args=m.tu.args,
|
|
127
|
-
),
|
|
128
|
-
)],
|
|
129
|
-
role='model',
|
|
130
|
-
))
|
|
131
|
-
|
|
132
|
-
else:
|
|
133
|
-
g_contents.append(pt.Content(
|
|
134
|
-
parts=[pt.Part(
|
|
135
|
-
text=check.not_none(self._get_msg_content(m)),
|
|
136
|
-
)],
|
|
137
|
-
role=self.ROLES_MAP[type(m)], # type: ignore[arg-type]
|
|
138
|
-
))
|
|
139
|
-
|
|
140
62
|
g_tools: list[pt.Tool] = []
|
|
141
63
|
with tv.TypedValues(*request.options).consume() as oc:
|
|
142
64
|
t: Tool
|
|
@@ -145,10 +67,17 @@ class GoogleChatChoicesService:
|
|
|
145
67
|
function_declarations=[build_tool_spec_schema(t.spec)],
|
|
146
68
|
))
|
|
147
69
|
|
|
70
|
+
msgs = list(request.v)
|
|
71
|
+
|
|
72
|
+
system_inst = pop_system_instructions(msgs)
|
|
73
|
+
|
|
148
74
|
g_req = pt.GenerateContentRequest(
|
|
149
|
-
contents=
|
|
75
|
+
contents=[
|
|
76
|
+
make_msg_content(m)
|
|
77
|
+
for m in msgs
|
|
78
|
+
] or None,
|
|
150
79
|
tools=g_tools or None,
|
|
151
|
-
system_instruction=
|
|
80
|
+
system_instruction=system_inst,
|
|
152
81
|
)
|
|
153
82
|
|
|
154
83
|
req_dct = msh.marshal(g_req)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import typing as ta
|
|
2
|
+
|
|
3
|
+
from omlish import check
|
|
4
|
+
|
|
5
|
+
from .....backends.google.protocol import types as pt
|
|
6
|
+
from ....chat.messages import AiMessage
|
|
7
|
+
from ....chat.messages import Message
|
|
8
|
+
from ....chat.messages import SystemMessage
|
|
9
|
+
from ....chat.messages import ToolUseMessage
|
|
10
|
+
from ....chat.messages import ToolUseResultMessage
|
|
11
|
+
from ....chat.messages import UserMessage
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
ROLES_MAP: ta.Mapping[type[Message], pt.ContentRole | None] = { # noqa
|
|
18
|
+
SystemMessage: None,
|
|
19
|
+
UserMessage: 'user',
|
|
20
|
+
AiMessage: 'model',
|
|
21
|
+
ToolUseMessage: 'model',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def make_str_content(
|
|
26
|
+
s: str | None,
|
|
27
|
+
*,
|
|
28
|
+
role: pt.ContentRole | None = None,
|
|
29
|
+
) -> pt.Content | None:
|
|
30
|
+
if s is None:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
return pt.Content(
|
|
34
|
+
parts=[pt.Part(
|
|
35
|
+
text=check.not_none(s),
|
|
36
|
+
)],
|
|
37
|
+
role=role,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def make_msg_content(m: Message) -> pt.Content:
|
|
42
|
+
if isinstance(m, (AiMessage, SystemMessage, UserMessage)):
|
|
43
|
+
return check.not_none(make_str_content(
|
|
44
|
+
check.isinstance(m.c, str),
|
|
45
|
+
role=ROLES_MAP[type(m)],
|
|
46
|
+
))
|
|
47
|
+
|
|
48
|
+
elif isinstance(m, ToolUseResultMessage):
|
|
49
|
+
tr_resp_val: pt.Value
|
|
50
|
+
if m.tur.c is None:
|
|
51
|
+
tr_resp_val = pt.NullValue() # type: ignore[unreachable]
|
|
52
|
+
elif isinstance(m.tur.c, str):
|
|
53
|
+
tr_resp_val = pt.StringValue(m.tur.c)
|
|
54
|
+
else:
|
|
55
|
+
raise TypeError(m.tur.c)
|
|
56
|
+
|
|
57
|
+
return pt.Content(
|
|
58
|
+
parts=[pt.Part(
|
|
59
|
+
function_response=pt.FunctionResponse(
|
|
60
|
+
id=m.tur.id,
|
|
61
|
+
name=m.tur.name,
|
|
62
|
+
response={
|
|
63
|
+
'value': tr_resp_val,
|
|
64
|
+
},
|
|
65
|
+
),
|
|
66
|
+
)],
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
elif isinstance(m, ToolUseMessage):
|
|
70
|
+
return pt.Content(
|
|
71
|
+
parts=[pt.Part(
|
|
72
|
+
function_call=pt.FunctionCall(
|
|
73
|
+
id=m.tu.id,
|
|
74
|
+
name=m.tu.name,
|
|
75
|
+
args=m.tu.args,
|
|
76
|
+
),
|
|
77
|
+
)],
|
|
78
|
+
role='model',
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
else:
|
|
82
|
+
raise TypeError(m)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def pop_system_instructions(msgs: list[Message]) -> pt.Content | None:
|
|
86
|
+
if not msgs:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
m0 = msgs[0]
|
|
90
|
+
if not isinstance(m0 := msgs[0], SystemMessage):
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
msgs.pop(0)
|
|
94
|
+
return make_msg_content(m0)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_msg_content(m: Message) -> str | None:
|
|
98
|
+
if isinstance(m, AiMessage):
|
|
99
|
+
return check.isinstance(m.c, str)
|
|
100
|
+
|
|
101
|
+
elif isinstance(m, (SystemMessage, UserMessage)):
|
|
102
|
+
return check.isinstance(m.c, str)
|
|
103
|
+
|
|
104
|
+
else:
|
|
105
|
+
raise TypeError(m)
|
|
@@ -8,7 +8,7 @@ from omlish import marshal as msh
|
|
|
8
8
|
from omlish import typedvalues as tv
|
|
9
9
|
from omlish.formats import json
|
|
10
10
|
from omlish.http import all as http
|
|
11
|
-
from omlish.
|
|
11
|
+
from omlish.http import sse
|
|
12
12
|
|
|
13
13
|
from .....backends.google.protocol import types as pt
|
|
14
14
|
from ....chat.choices.stream.services import ChatChoicesStreamRequest
|
|
@@ -16,22 +16,16 @@ from ....chat.choices.stream.services import ChatChoicesStreamResponse
|
|
|
16
16
|
from ....chat.choices.stream.services import static_check_is_chat_choices_stream_service
|
|
17
17
|
from ....chat.choices.stream.types import AiChoiceDeltas
|
|
18
18
|
from ....chat.choices.stream.types import AiChoicesDeltas
|
|
19
|
-
from ....chat.choices.types import ChatChoicesOutputs
|
|
20
|
-
from ....chat.messages import AiMessage
|
|
21
|
-
from ....chat.messages import Message
|
|
22
|
-
from ....chat.messages import SystemMessage
|
|
23
|
-
from ....chat.messages import ToolUseMessage
|
|
24
|
-
from ....chat.messages import ToolUseResultMessage
|
|
25
|
-
from ....chat.messages import UserMessage
|
|
26
19
|
from ....chat.stream.types import ContentAiDelta
|
|
27
20
|
from ....chat.stream.types import ToolUseAiDelta
|
|
28
21
|
from ....chat.tools.types import Tool
|
|
22
|
+
from ....http.stream import BytesHttpStreamResponseBuilder
|
|
23
|
+
from ....http.stream import SimpleSseLinesHttpStreamResponseHandler
|
|
29
24
|
from ....models.configs import ModelName
|
|
30
|
-
from ....resources import UseResources
|
|
31
25
|
from ....standard import ApiKey
|
|
32
|
-
from ....stream.services import StreamResponseSink
|
|
33
|
-
from ....stream.services import new_stream_response
|
|
34
26
|
from .names import MODEL_NAMES
|
|
27
|
+
from .protocol import make_msg_content
|
|
28
|
+
from .protocol import pop_system_instructions
|
|
35
29
|
from .tools import build_tool_spec_schema
|
|
36
30
|
|
|
37
31
|
|
|
@@ -59,71 +53,38 @@ class GoogleChatChoicesStreamService:
|
|
|
59
53
|
self._model_name = cc.pop(self.DEFAULT_MODEL_NAME)
|
|
60
54
|
self._api_key = ApiKey.pop_secret(cc, env='GEMINI_API_KEY')
|
|
61
55
|
|
|
62
|
-
def _make_str_content(
|
|
63
|
-
self,
|
|
64
|
-
s: str | None,
|
|
65
|
-
*,
|
|
66
|
-
role: pt.ContentRole | None = None,
|
|
67
|
-
) -> pt.Content | None:
|
|
68
|
-
if s is None:
|
|
69
|
-
return None
|
|
70
|
-
|
|
71
|
-
return pt.Content(
|
|
72
|
-
parts=[pt.Part(
|
|
73
|
-
text=check.not_none(s),
|
|
74
|
-
)],
|
|
75
|
-
role=role,
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
def _make_msg_content(self, m: Message) -> pt.Content:
|
|
79
|
-
if isinstance(m, (AiMessage, SystemMessage, UserMessage)):
|
|
80
|
-
return check.not_none(self._make_str_content(
|
|
81
|
-
check.isinstance(m.c, str),
|
|
82
|
-
role=self.ROLES_MAP[type(m)],
|
|
83
|
-
))
|
|
84
|
-
|
|
85
|
-
elif isinstance(m, ToolUseResultMessage):
|
|
86
|
-
tr_resp_val: pt.Value
|
|
87
|
-
if m.tur.c is None:
|
|
88
|
-
tr_resp_val = pt.NullValue() # type: ignore[unreachable]
|
|
89
|
-
elif isinstance(m.tur.c, str):
|
|
90
|
-
tr_resp_val = pt.StringValue(m.tur.c)
|
|
91
|
-
else:
|
|
92
|
-
raise TypeError(m.tur.c)
|
|
93
|
-
return pt.Content(
|
|
94
|
-
parts=[pt.Part(
|
|
95
|
-
function_response=pt.FunctionResponse(
|
|
96
|
-
id=m.tur.id,
|
|
97
|
-
name=m.tur.name,
|
|
98
|
-
response={
|
|
99
|
-
'value': tr_resp_val,
|
|
100
|
-
},
|
|
101
|
-
),
|
|
102
|
-
)],
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
elif isinstance(m, ToolUseMessage):
|
|
106
|
-
return pt.Content(
|
|
107
|
-
parts=[pt.Part(
|
|
108
|
-
function_call=pt.FunctionCall(
|
|
109
|
-
id=m.tu.id,
|
|
110
|
-
name=m.tu.name,
|
|
111
|
-
args=m.tu.args,
|
|
112
|
-
),
|
|
113
|
-
)],
|
|
114
|
-
role='model',
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
else:
|
|
118
|
-
raise TypeError(m)
|
|
119
|
-
|
|
120
56
|
BASE_URL: ta.ClassVar[str] = 'https://generativelanguage.googleapis.com/v1beta/models'
|
|
121
57
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
58
|
+
def _process_sse(self, so: sse.SseDecoderOutput) -> ta.Iterable[AiChoicesDeltas | None]:
|
|
59
|
+
if not (isinstance(so, sse.SseEvent) and so.type == b'message'):
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
gcr = msh.unmarshal(json.loads(so.data.decode('utf-8')), pt.GenerateContentResponse) # noqa
|
|
63
|
+
cnd = check.single(check.not_none(gcr.candidates))
|
|
64
|
+
|
|
65
|
+
for p in check.not_none(cnd.content).parts or []:
|
|
66
|
+
if (txt := p.text) is not None:
|
|
67
|
+
check.none(p.function_call)
|
|
68
|
+
yield AiChoicesDeltas([
|
|
69
|
+
AiChoiceDeltas([
|
|
70
|
+
ContentAiDelta(check.not_none(txt)),
|
|
71
|
+
]),
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
elif (fc := p.function_call) is not None:
|
|
75
|
+
check.none(p.text)
|
|
76
|
+
yield AiChoicesDeltas([
|
|
77
|
+
AiChoiceDeltas([
|
|
78
|
+
ToolUseAiDelta(
|
|
79
|
+
id=fc.id,
|
|
80
|
+
name=fc.name,
|
|
81
|
+
args=fc.args,
|
|
82
|
+
),
|
|
83
|
+
]),
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
else:
|
|
87
|
+
raise ValueError(p)
|
|
127
88
|
|
|
128
89
|
READ_CHUNK_SIZE: ta.ClassVar[int] = -1
|
|
129
90
|
|
|
@@ -133,13 +94,6 @@ class GoogleChatChoicesStreamService:
|
|
|
133
94
|
) -> ChatChoicesStreamResponse:
|
|
134
95
|
key = check.not_none(self._api_key).reveal()
|
|
135
96
|
|
|
136
|
-
msgs = list(request.v)
|
|
137
|
-
|
|
138
|
-
system_inst: pt.Content | None = None
|
|
139
|
-
if msgs and isinstance(m0 := msgs[0], SystemMessage):
|
|
140
|
-
system_inst = self._make_msg_content(m0)
|
|
141
|
-
msgs.pop(0)
|
|
142
|
-
|
|
143
97
|
g_tools: list[pt.Tool] = []
|
|
144
98
|
with tv.TypedValues(*request.options).consume() as oc:
|
|
145
99
|
t: Tool
|
|
@@ -148,11 +102,15 @@ class GoogleChatChoicesStreamService:
|
|
|
148
102
|
function_declarations=[build_tool_spec_schema(t.spec)],
|
|
149
103
|
))
|
|
150
104
|
|
|
105
|
+
msgs = list(request.v)
|
|
106
|
+
|
|
107
|
+
system_inst = pop_system_instructions(msgs)
|
|
108
|
+
|
|
151
109
|
g_req = pt.GenerateContentRequest(
|
|
152
110
|
contents=[
|
|
153
|
-
|
|
111
|
+
make_msg_content(m)
|
|
154
112
|
for m in msgs
|
|
155
|
-
],
|
|
113
|
+
] or None,
|
|
156
114
|
tools=g_tools or None,
|
|
157
115
|
system_instruction=system_inst,
|
|
158
116
|
)
|
|
@@ -168,52 +126,11 @@ class GoogleChatChoicesStreamService:
|
|
|
168
126
|
method='POST',
|
|
169
127
|
)
|
|
170
128
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
http_response
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
for bl in db.feed(b):
|
|
180
|
-
if isinstance(bl, DelimitingBuffer.Incomplete):
|
|
181
|
-
# FIXME: handle
|
|
182
|
-
raise TypeError(bl)
|
|
183
|
-
|
|
184
|
-
l = bl.decode('utf-8')
|
|
185
|
-
if not l:
|
|
186
|
-
continue
|
|
187
|
-
|
|
188
|
-
if l.startswith('data: '):
|
|
189
|
-
gcr = msh.unmarshal(json.loads(l[6:]), pt.GenerateContentResponse) # noqa
|
|
190
|
-
cnd = check.single(check.not_none(gcr.candidates))
|
|
191
|
-
|
|
192
|
-
for p in check.not_none(cnd.content).parts or []:
|
|
193
|
-
if (txt := p.text) is not None:
|
|
194
|
-
check.none(p.function_call)
|
|
195
|
-
await sink.emit(AiChoicesDeltas([
|
|
196
|
-
AiChoiceDeltas([
|
|
197
|
-
ContentAiDelta(check.not_none(txt)),
|
|
198
|
-
]),
|
|
199
|
-
]))
|
|
200
|
-
|
|
201
|
-
elif (fc := p.function_call) is not None:
|
|
202
|
-
check.none(p.text)
|
|
203
|
-
await sink.emit(AiChoicesDeltas([
|
|
204
|
-
AiChoiceDeltas([
|
|
205
|
-
ToolUseAiDelta(
|
|
206
|
-
id=fc.id,
|
|
207
|
-
name=fc.name,
|
|
208
|
-
args=fc.args,
|
|
209
|
-
),
|
|
210
|
-
]),
|
|
211
|
-
]))
|
|
212
|
-
|
|
213
|
-
else:
|
|
214
|
-
raise ValueError(p)
|
|
215
|
-
|
|
216
|
-
if not b:
|
|
217
|
-
return []
|
|
218
|
-
|
|
219
|
-
return await new_stream_response(rs, inner)
|
|
129
|
+
return await BytesHttpStreamResponseBuilder(
|
|
130
|
+
self._http_client,
|
|
131
|
+
lambda http_response: SimpleSseLinesHttpStreamResponseHandler(self._process_sse).as_lines().as_bytes(),
|
|
132
|
+
read_chunk_size=self.READ_CHUNK_SIZE,
|
|
133
|
+
).new_stream_response(
|
|
134
|
+
http_request,
|
|
135
|
+
request.options,
|
|
136
|
+
)
|