tessaract 0.1.0__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.
- tessaract/__init__.py +16 -0
- tessaract/adapters/__init__.py +3 -0
- tessaract/adapters/adapter.py +55 -0
- tessaract/adapters/openai/openai_adapter.py +348 -0
- tessaract/client.py +151 -0
- tessaract/providers/__init__.py +7 -0
- tessaract/providers/openai_provider.py +35 -0
- tessaract/providers/provider.py +36 -0
- tessaract/tools/__init__.py +7 -0
- tessaract/tools/function.py +71 -0
- tessaract/types/__init__.py +1 -0
- tessaract/types/input_types.py +27 -0
- tessaract/types/output_types.py +60 -0
- tessaract/types/request.py +33 -0
- tessaract/types/response.py +73 -0
- tessaract/types/streaming/event_types.py +146 -0
- tessaract/types/types.py +0 -0
- tessaract-0.1.0.dist-info/METADATA +277 -0
- tessaract-0.1.0.dist-info/RECORD +21 -0
- tessaract-0.1.0.dist-info/WHEEL +4 -0
- tessaract-0.1.0.dist-info/licenses/LICENSE +9 -0
tessaract/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .client import Tessaract
|
|
2
|
+
from .providers.openai_provider import OpenAIProvider
|
|
3
|
+
from .tools.function import FunctionTool, InputSchema, Property
|
|
4
|
+
from .types.input_types import FunctionToolResult, UserMessage
|
|
5
|
+
from .types.request import ReasoningOptions
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"FunctionTool",
|
|
9
|
+
"FunctionToolResult",
|
|
10
|
+
"InputSchema",
|
|
11
|
+
"OpenAIProvider",
|
|
12
|
+
"Property",
|
|
13
|
+
"ReasoningOptions",
|
|
14
|
+
"Tessaract",
|
|
15
|
+
"UserMessage"
|
|
16
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from typing import Any, Literal
|
|
2
|
+
|
|
3
|
+
from typing_extensions import Protocol
|
|
4
|
+
|
|
5
|
+
from ..providers.provider import Provider
|
|
6
|
+
from ..tools.function import InputSchema
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UserMessageProtocol(Protocol):
|
|
10
|
+
role: Literal["user"] = "user"
|
|
11
|
+
content: str | list[dict]
|
|
12
|
+
|
|
13
|
+
class FunctionToolResultProtocol(Protocol):
|
|
14
|
+
type: Literal["function_tool_result"] = "function_tool_result"
|
|
15
|
+
call_id: str
|
|
16
|
+
result: Any
|
|
17
|
+
is_error: bool = False
|
|
18
|
+
|
|
19
|
+
class FunctionToolSchemaProtocol(Protocol):
|
|
20
|
+
name: str
|
|
21
|
+
description: str
|
|
22
|
+
input_schema: InputSchema | None
|
|
23
|
+
strict: bool | None = None
|
|
24
|
+
provider_options: dict[str, Any]
|
|
25
|
+
|
|
26
|
+
class ReasoningParamsProtocol(Protocol):
|
|
27
|
+
effort: Literal["none", "minimal", "low", "medium", "high", "extra_high", "max"] | None = None
|
|
28
|
+
summary: Literal["concise", "auto", "detailed"] | None = None
|
|
29
|
+
mode: Literal["standard", "pro"] | None = None
|
|
30
|
+
|
|
31
|
+
class ReasoningProtocol(Protocol):
|
|
32
|
+
type: Literal["reasoning"] = "reasoning"
|
|
33
|
+
text: str | None = None
|
|
34
|
+
|
|
35
|
+
class Adapter:
|
|
36
|
+
def __init__(self, provider: Provider):
|
|
37
|
+
self._provider = provider
|
|
38
|
+
|
|
39
|
+
def map_input_message(self, item: UserMessageProtocol) -> Any:
|
|
40
|
+
raise NotImplementedError("not yet implemented...")
|
|
41
|
+
|
|
42
|
+
def map_tool_result(self, item: FunctionToolResultProtocol) -> Any:
|
|
43
|
+
raise NotImplementedError("not yet implemented...")
|
|
44
|
+
|
|
45
|
+
def map_function_schema(self, tools: list[FunctionToolSchemaProtocol]) -> list:
|
|
46
|
+
raise NotImplementedError("not yet implemented...")
|
|
47
|
+
|
|
48
|
+
def map_reasoning_params(self, item: ReasoningParamsProtocol) -> Any:
|
|
49
|
+
raise NotImplementedError("not yet implemented...")
|
|
50
|
+
|
|
51
|
+
def map_reasoning(self, item: ReasoningProtocol) -> Any:
|
|
52
|
+
raise NotImplementedError("not yet implemented")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections.abc import Iterator, Sequence
|
|
3
|
+
|
|
4
|
+
from openai import Omit
|
|
5
|
+
from openai.types.shared_params import Reasoning as OpenAIReasoningParams
|
|
6
|
+
|
|
7
|
+
from ...providers.openai_provider import OpenAIProvider
|
|
8
|
+
from ...tools.function import InputSchema, Property
|
|
9
|
+
from ...types.output_types import (
|
|
10
|
+
AssistantMessage,
|
|
11
|
+
FunctionCallOutputItem,
|
|
12
|
+
ProviderOutputItem,
|
|
13
|
+
OutputType,
|
|
14
|
+
ReasoningOutputItem,
|
|
15
|
+
TextOutputItem,
|
|
16
|
+
)
|
|
17
|
+
from ...types.request import Request
|
|
18
|
+
from ...types.response import OpenAIResponse, ResponseError
|
|
19
|
+
from ...types.streaming.event_types import (
|
|
20
|
+
CustomProviderEvent,
|
|
21
|
+
FunctionCallArgumentDeltaEvent,
|
|
22
|
+
OutputItemCompletedEvent,
|
|
23
|
+
ReasoningStartedEvent,
|
|
24
|
+
ReasoningSummaryDeltaEvent,
|
|
25
|
+
ReasoningTextDeltaEvent,
|
|
26
|
+
ResponseCompletedEvent,
|
|
27
|
+
ResponseFailedEvent,
|
|
28
|
+
ResponseStartedEvent,
|
|
29
|
+
StreamEventUnion,
|
|
30
|
+
TextDeltaEvent,
|
|
31
|
+
ToolCallStartedEvent,
|
|
32
|
+
)
|
|
33
|
+
from ..adapter import (
|
|
34
|
+
Adapter,
|
|
35
|
+
FunctionToolResultProtocol,
|
|
36
|
+
FunctionToolSchemaProtocol,
|
|
37
|
+
ReasoningParamsProtocol,
|
|
38
|
+
UserMessageProtocol,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class OpenAIAdapter(Adapter):
|
|
43
|
+
def __init__(self, provider: OpenAIProvider):
|
|
44
|
+
super().__init__(provider)
|
|
45
|
+
self._client = provider.client
|
|
46
|
+
|
|
47
|
+
def map_input_message(self, item: UserMessageProtocol):
|
|
48
|
+
return {
|
|
49
|
+
"role": item.role,
|
|
50
|
+
"content": item.content
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
def map_reasoning_params(self, reasoning: ReasoningParamsProtocol | None) -> OpenAIReasoningParams | Omit | None:
|
|
54
|
+
if reasoning is None:
|
|
55
|
+
return Omit()
|
|
56
|
+
|
|
57
|
+
native_reasoning: OpenAIReasoningParams = {}
|
|
58
|
+
|
|
59
|
+
if reasoning.mode is not None:
|
|
60
|
+
native_reasoning["mode"] = reasoning.mode
|
|
61
|
+
|
|
62
|
+
if reasoning.summary is not None:
|
|
63
|
+
native_reasoning["summary"] = reasoning.summary
|
|
64
|
+
|
|
65
|
+
if reasoning.effort is not None:
|
|
66
|
+
native_reasoning["effort"] = (
|
|
67
|
+
"xhigh" if reasoning.effort == "extra_high" else reasoning.effort
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return native_reasoning
|
|
71
|
+
|
|
72
|
+
def _native_property(self, prop: Property) -> dict:
|
|
73
|
+
native: dict[str, object] = {"type": prop.type}
|
|
74
|
+
|
|
75
|
+
if prop.description is not None:
|
|
76
|
+
native["description"] = prop.description
|
|
77
|
+
|
|
78
|
+
if prop.enum is not None:
|
|
79
|
+
native["enum"] = prop.enum
|
|
80
|
+
|
|
81
|
+
if prop.items is not None:
|
|
82
|
+
native["items"] = self._native_property(prop.items)
|
|
83
|
+
|
|
84
|
+
if prop.properties is not None:
|
|
85
|
+
native["properties"] = {
|
|
86
|
+
name: self._native_property(nested)
|
|
87
|
+
for name, nested in prop.properties.items()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if prop.required is not None:
|
|
91
|
+
native["required"] = prop.required
|
|
92
|
+
|
|
93
|
+
# Strict mode requires additionalProperties: false on every object
|
|
94
|
+
if "object" in prop._types():
|
|
95
|
+
native["additionalProperties"] = prop.additionalProperties if prop.additionalProperties is not None else False
|
|
96
|
+
|
|
97
|
+
return native
|
|
98
|
+
|
|
99
|
+
def _native_tool_parameters(self, input_schema: InputSchema):
|
|
100
|
+
_properties = {
|
|
101
|
+
prop_name: self._native_property(prop_schema)
|
|
102
|
+
for prop_name, prop_schema in input_schema.properties.items()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
"type": input_schema.type,
|
|
107
|
+
"properties": _properties,
|
|
108
|
+
"required": input_schema.required,
|
|
109
|
+
"additionalProperties": input_schema.additionalProperties if input_schema.additionalProperties is not None else False
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
def map_function_schema(self, tools: Sequence[FunctionToolSchemaProtocol]) -> list:
|
|
113
|
+
|
|
114
|
+
_native_tools_list = []
|
|
115
|
+
for tool in tools:
|
|
116
|
+
_native_tool_schema: dict[str, object] = {}
|
|
117
|
+
|
|
118
|
+
_native_tool_schema["type"] = "function"
|
|
119
|
+
_native_tool_schema["name"] = tool.name
|
|
120
|
+
_native_tool_schema["description"] = tool.description
|
|
121
|
+
_native_tool_schema["strict"] = tool.strict if tool.strict is not None else True
|
|
122
|
+
|
|
123
|
+
if tool.input_schema is not None:
|
|
124
|
+
_native_tool_schema["parameters"] = self._native_tool_parameters(tool.input_schema) if tool.input_schema is not None else None
|
|
125
|
+
|
|
126
|
+
canonical_keys = {
|
|
127
|
+
"type",
|
|
128
|
+
"name",
|
|
129
|
+
"description",
|
|
130
|
+
"strict",
|
|
131
|
+
"parameters"
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
extra_fields = {
|
|
135
|
+
key: value
|
|
136
|
+
for key, value in tool.provider_options.items()
|
|
137
|
+
if key not in canonical_keys
|
|
138
|
+
}
|
|
139
|
+
_native_tools_list.append({**extra_fields, **_native_tool_schema})
|
|
140
|
+
return _native_tools_list
|
|
141
|
+
|
|
142
|
+
def _is_content_parts(self, result) -> bool:
|
|
143
|
+
return isinstance(result, list) and all(
|
|
144
|
+
isinstance(part, dict) and "type" in part for part in result
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def map_tool_result(self, item: FunctionToolResultProtocol):
|
|
148
|
+
# OpenAI has no error flag on function_call_output, so errors are marked in the output itself
|
|
149
|
+
if item.is_error:
|
|
150
|
+
output = json.dumps({"error": item.result}, default=str)
|
|
151
|
+
|
|
152
|
+
elif isinstance(item.result, str) or self._is_content_parts(item.result):
|
|
153
|
+
output = item.result
|
|
154
|
+
|
|
155
|
+
else:
|
|
156
|
+
output = json.dumps(item.result, default=str)
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
"type": "function_call_output",
|
|
160
|
+
"call_id": item.call_id,
|
|
161
|
+
"output": output
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _normalize_output_item(self, output_item) -> OutputType:
|
|
166
|
+
if output_item.type == "message" and (
|
|
167
|
+
output_item.content is None or any(
|
|
168
|
+
part.type != "output_text"
|
|
169
|
+
or bool(getattr(part, "annotations", []))
|
|
170
|
+
for part in output_item.content
|
|
171
|
+
)
|
|
172
|
+
):
|
|
173
|
+
return ProviderOutputItem(raw=output_item, provider_type=output_item.type)
|
|
174
|
+
|
|
175
|
+
match output_item.type:
|
|
176
|
+
case "message":
|
|
177
|
+
return AssistantMessage(
|
|
178
|
+
raw=output_item,
|
|
179
|
+
content=[
|
|
180
|
+
TextOutputItem(
|
|
181
|
+
raw=i,
|
|
182
|
+
text=i.text,
|
|
183
|
+
annotations=i.annotations
|
|
184
|
+
)
|
|
185
|
+
for i in output_item.content
|
|
186
|
+
]
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
case "reasoning":
|
|
190
|
+
return ReasoningOutputItem(
|
|
191
|
+
raw=output_item,
|
|
192
|
+
id=output_item.id,
|
|
193
|
+
content="".join(part.text for part in output_item.content),
|
|
194
|
+
text="".join(part.text for part in output_item.summary)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
case "function_call":
|
|
198
|
+
return FunctionCallOutputItem(
|
|
199
|
+
raw=output_item,
|
|
200
|
+
call_id=output_item.call_id,
|
|
201
|
+
name=output_item.name,
|
|
202
|
+
arguments=json.loads(output_item.arguments)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
case _:
|
|
206
|
+
raise ValueError(f"Unsupported OpenAI output item type: {output_item.type!r}")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _normalize_output(self, output_items) -> list[OutputType]:
|
|
210
|
+
return [self._normalize_output_item(item) for item in output_items]
|
|
211
|
+
|
|
212
|
+
def _normalize_stream_event(self, event):
|
|
213
|
+
match event.type:
|
|
214
|
+
case "response.created":
|
|
215
|
+
yield ResponseStartedEvent(
|
|
216
|
+
raw_event=event
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
case "response.completed":
|
|
220
|
+
yield ResponseCompletedEvent(
|
|
221
|
+
response=self._normalize_response(event.response),
|
|
222
|
+
raw_event=event
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
case "response.failed":
|
|
226
|
+
response = self._normalize_response(event.response)
|
|
227
|
+
error = response.error or ResponseError(message="Response failed")
|
|
228
|
+
yield ResponseFailedEvent(
|
|
229
|
+
message=error.message,
|
|
230
|
+
error=error,
|
|
231
|
+
response=response,
|
|
232
|
+
raw_event=event
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
case "error":
|
|
236
|
+
yield ResponseFailedEvent(
|
|
237
|
+
message=event.message,
|
|
238
|
+
error=ResponseError(message=event.message, code=event.code),
|
|
239
|
+
raw_event=event
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
case "response.output_item.added" if event.item.type == "function_call":
|
|
243
|
+
yield ToolCallStartedEvent(
|
|
244
|
+
call_id=event.item.call_id,
|
|
245
|
+
name=event.item.name,
|
|
246
|
+
output_index=event.output_index,
|
|
247
|
+
raw_event=event
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
case "response.output_text.delta":
|
|
251
|
+
yield TextDeltaEvent(
|
|
252
|
+
delta=event.delta,
|
|
253
|
+
provider="openai",
|
|
254
|
+
output_index=event.output_index,
|
|
255
|
+
content_index=event.content_index,
|
|
256
|
+
raw_event=event
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
case "response.output_item.done":
|
|
260
|
+
yield OutputItemCompletedEvent(
|
|
261
|
+
item=self._normalize_output_item(event.item),
|
|
262
|
+
raw_event=event
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
case "response.reasoning_summary_text.delta":
|
|
266
|
+
yield ReasoningSummaryDeltaEvent(
|
|
267
|
+
delta=event.delta,
|
|
268
|
+
output_index=event.output_index,
|
|
269
|
+
item_id=event.item_id,
|
|
270
|
+
raw_event=event
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
case "response.reasoning_text.delta":
|
|
274
|
+
yield ReasoningTextDeltaEvent(
|
|
275
|
+
delta=event.delta,
|
|
276
|
+
output_index=event.output_index,
|
|
277
|
+
item_id=event.item_id,
|
|
278
|
+
raw_event=event
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
case "response.function_call_arguments.delta":
|
|
282
|
+
yield FunctionCallArgumentDeltaEvent(
|
|
283
|
+
delta=event.delta,
|
|
284
|
+
output_index=event.output_index,
|
|
285
|
+
raw_event=event
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
case "response.reasoning_summary_part.added":
|
|
289
|
+
yield ReasoningStartedEvent(
|
|
290
|
+
item_id=event.item_id,
|
|
291
|
+
raw_event=event
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
case _:
|
|
295
|
+
yield CustomProviderEvent(
|
|
296
|
+
type=event.type,
|
|
297
|
+
raw_event=event
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _normalize_response(self, raw_response) -> OpenAIResponse:
|
|
302
|
+
return OpenAIResponse(
|
|
303
|
+
id=raw_response.id,
|
|
304
|
+
model=raw_response.model,
|
|
305
|
+
status=raw_response.status,
|
|
306
|
+
provider=self._provider,
|
|
307
|
+
output=self._normalize_output(raw_response.output),
|
|
308
|
+
error=ResponseError(message=raw_response.error.message, code=raw_response.error.code) if raw_response.error else None,
|
|
309
|
+
raw_response=raw_response
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
def _build_request_kwargs(self, request: Request):
|
|
313
|
+
canonical_params = {
|
|
314
|
+
"model": request.model,
|
|
315
|
+
"input": request.input,
|
|
316
|
+
"tools": self.map_function_schema(request.tools or []),
|
|
317
|
+
"reasoning": self.map_reasoning_params(request.reasoning),
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
provider_options = dict(**request.provider_options or {})
|
|
321
|
+
|
|
322
|
+
extra_body = provider_options.get("extra_body")
|
|
323
|
+
|
|
324
|
+
if extra_body is not None:
|
|
325
|
+
provider_options["extra_body"] = {
|
|
326
|
+
key: value
|
|
327
|
+
for key, value in extra_body.items()
|
|
328
|
+
if key not in canonical_params
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
return {**provider_options, **canonical_params}
|
|
333
|
+
|
|
334
|
+
def generate_sync(self, request: Request) -> OpenAIResponse:
|
|
335
|
+
|
|
336
|
+
kwargs = self._build_request_kwargs(request)
|
|
337
|
+
|
|
338
|
+
_raw_response = self._client.responses.create(**kwargs, stream=False)
|
|
339
|
+
|
|
340
|
+
return self._normalize_response(_raw_response)
|
|
341
|
+
|
|
342
|
+
def generate_stream(self, request: Request) -> Iterator[StreamEventUnion]:
|
|
343
|
+
|
|
344
|
+
kwargs = self._build_request_kwargs(request)
|
|
345
|
+
|
|
346
|
+
with self._client.responses.stream(**kwargs) as stream:
|
|
347
|
+
for raw_event in stream:
|
|
348
|
+
yield from self._normalize_stream_event(raw_event)
|
tessaract/client.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from collections.abc import Iterator
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Literal, overload
|
|
3
|
+
|
|
4
|
+
from .providers import OpenAIProvider
|
|
5
|
+
from .tools.function import FunctionTool
|
|
6
|
+
from .types.input_types import InputType, UserMessage
|
|
7
|
+
from .types.output_types import AssistantMessage, OutputItem
|
|
8
|
+
from .types.request import ReasoningOptions, Request
|
|
9
|
+
from .types.response import Response
|
|
10
|
+
from .types.streaming.event_types import StreamEventUnion
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .adapters.openai.openai_adapter import OpenAIAdapter
|
|
14
|
+
|
|
15
|
+
class Tessaract:
|
|
16
|
+
def __init__(self, providers: dict[str, OpenAIProvider]):
|
|
17
|
+
self.providers = providers
|
|
18
|
+
self.adapters: dict[str, "OpenAIAdapter" ] = {} # value should be a union of OpenAIAdapter | AnthropicAdapter once implemented
|
|
19
|
+
self.register_adapter()
|
|
20
|
+
|
|
21
|
+
def register_adapter(self):
|
|
22
|
+
for prefix, provider in self.providers.items():
|
|
23
|
+
if isinstance(provider, OpenAIProvider):
|
|
24
|
+
from .adapters.openai.openai_adapter import OpenAIAdapter
|
|
25
|
+
adapter = OpenAIAdapter(provider)
|
|
26
|
+
else:
|
|
27
|
+
raise NotImplementedError("not yet implemented")
|
|
28
|
+
self.adapters[prefix] = adapter
|
|
29
|
+
|
|
30
|
+
def _normalize_model_name(self, model: str):
|
|
31
|
+
model_prefix, separator, model_name = model.partition("/")
|
|
32
|
+
|
|
33
|
+
if not separator or not model_prefix or not model_name:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"model must be of the form '<provider prefix>/<model name>', got {model!r}"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return (model_prefix, model_name)
|
|
39
|
+
|
|
40
|
+
def _build_request_model(
|
|
41
|
+
self,
|
|
42
|
+
model: str,
|
|
43
|
+
provider: str,
|
|
44
|
+
input: str | list[str | InputType],
|
|
45
|
+
reasoning: ReasoningOptions | None,
|
|
46
|
+
tools: list[FunctionTool],
|
|
47
|
+
request_options: dict[str, Any],
|
|
48
|
+
stream: bool
|
|
49
|
+
) -> Request:
|
|
50
|
+
|
|
51
|
+
all_items = []
|
|
52
|
+
|
|
53
|
+
if isinstance(input, str):
|
|
54
|
+
converted_item = UserMessage(content=input)
|
|
55
|
+
all_items.append(converted_item.raw(self.adapters[provider]))
|
|
56
|
+
|
|
57
|
+
else:
|
|
58
|
+
for message in input:
|
|
59
|
+
if isinstance(message, str):
|
|
60
|
+
converted_item = UserMessage(content=message)
|
|
61
|
+
all_items.append(converted_item.raw(self.adapters[provider]))
|
|
62
|
+
|
|
63
|
+
elif isinstance(message, InputType):
|
|
64
|
+
all_items.append(message.raw(self.adapters[provider]))
|
|
65
|
+
|
|
66
|
+
elif isinstance(message, AssistantMessage): # noqa: SIM114
|
|
67
|
+
all_items.append(message.raw)
|
|
68
|
+
|
|
69
|
+
elif isinstance(message, OutputItem):
|
|
70
|
+
all_items.append(message.raw)
|
|
71
|
+
|
|
72
|
+
else:
|
|
73
|
+
raise TypeError(f"Unsupported input type: {type(message).__name__}")
|
|
74
|
+
|
|
75
|
+
return Request(
|
|
76
|
+
model=model,
|
|
77
|
+
input=all_items,
|
|
78
|
+
reasoning=reasoning,
|
|
79
|
+
tools=tools,
|
|
80
|
+
provider_options=request_options,
|
|
81
|
+
stream=stream
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@overload
|
|
85
|
+
def send(
|
|
86
|
+
self, model: str,
|
|
87
|
+
input: str | list[str | InputType],
|
|
88
|
+
stream: Literal[False],
|
|
89
|
+
reasoning: ReasoningOptions | None = None,
|
|
90
|
+
tools: list[FunctionTool] | None = None,
|
|
91
|
+
request_options: dict[str, Any] | None = None
|
|
92
|
+
) -> Response: ...
|
|
93
|
+
|
|
94
|
+
@overload
|
|
95
|
+
def send(
|
|
96
|
+
self, model: str,
|
|
97
|
+
input: str | list[str | InputType],
|
|
98
|
+
stream: Literal[True],
|
|
99
|
+
reasoning: ReasoningOptions | None = None,
|
|
100
|
+
tools: list[FunctionTool] | None = None,
|
|
101
|
+
request_options: dict[str, Any] | None = None
|
|
102
|
+
) -> Iterator[StreamEventUnion]: ...
|
|
103
|
+
|
|
104
|
+
@overload
|
|
105
|
+
def send(
|
|
106
|
+
self,
|
|
107
|
+
model: str,
|
|
108
|
+
input: str | list[str | InputType],
|
|
109
|
+
stream: bool,
|
|
110
|
+
reasoning: ReasoningOptions | None = None,
|
|
111
|
+
tools: list[FunctionTool] | None = None,
|
|
112
|
+
request_options: dict[str, Any] | None = None,
|
|
113
|
+
) -> Response | Iterator[StreamEventUnion]: ...
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def send(
|
|
117
|
+
self, model: str,
|
|
118
|
+
input: str | list[str | InputType],
|
|
119
|
+
stream: bool = False,
|
|
120
|
+
reasoning: ReasoningOptions | None = None,
|
|
121
|
+
tools: list[FunctionTool] | None = None,
|
|
122
|
+
request_options: dict[str, Any] | None = None
|
|
123
|
+
|
|
124
|
+
) -> Response | Iterator[StreamEventUnion]:
|
|
125
|
+
|
|
126
|
+
provider, model = self._normalize_model_name(model=model)
|
|
127
|
+
|
|
128
|
+
if provider not in self.providers:
|
|
129
|
+
raise ValueError("provider prefix must match registered provider in tessaract object")
|
|
130
|
+
|
|
131
|
+
_request_provider = self.providers[provider]
|
|
132
|
+
|
|
133
|
+
_tools = tools if tools is not None else []
|
|
134
|
+
|
|
135
|
+
# if reasoning is not None:
|
|
136
|
+
# _reasoning = reasoning
|
|
137
|
+
|
|
138
|
+
_request_options = request_options if request_options is not None else {}
|
|
139
|
+
|
|
140
|
+
_tessaract_request = self._build_request_model(model=model, input=input, provider=provider, reasoning=reasoning, tools=_tools, request_options=_request_options, stream=stream)
|
|
141
|
+
|
|
142
|
+
if isinstance(_request_provider, OpenAIProvider):
|
|
143
|
+
|
|
144
|
+
adapter = self.adapters[provider]
|
|
145
|
+
|
|
146
|
+
if stream == True:
|
|
147
|
+
return adapter.generate_stream(request=_tessaract_request)
|
|
148
|
+
|
|
149
|
+
return adapter.generate_sync(request=_tessaract_request)
|
|
150
|
+
|
|
151
|
+
raise NotImplementedError("Unsupported provider")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import TYPE_CHECKING, cast
|
|
3
|
+
|
|
4
|
+
from .provider import Provider
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from openai import OpenAI
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OpenAIProvider(Provider):
|
|
11
|
+
def __post_init__(self) -> None:
|
|
12
|
+
try:
|
|
13
|
+
from openai import OpenAI
|
|
14
|
+
except ModuleNotFoundError as exc:
|
|
15
|
+
if exc.name != "openai":
|
|
16
|
+
raise
|
|
17
|
+
raise ImportError(
|
|
18
|
+
"OpenAI support requires the optional dependency. "
|
|
19
|
+
'Install it with: pip install "tessaract[openai]"'
|
|
20
|
+
) from exc
|
|
21
|
+
|
|
22
|
+
if self.api_key is None:
|
|
23
|
+
self.api_key = os.environ.get("OPENAI_API_KEY")
|
|
24
|
+
|
|
25
|
+
if self.api_key is None:
|
|
26
|
+
raise ValueError(
|
|
27
|
+
"No OpenAI API key found. Pass api_key to OpenAIProvider "
|
|
28
|
+
"or set the OPENAI_API_KEY environment variable."
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
self._client = OpenAI(**self._client_kwargs())
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def client(self) -> "OpenAI":
|
|
35
|
+
return cast("OpenAI", self._client)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class Provider:
|
|
6
|
+
api_key: str | None = None
|
|
7
|
+
base_url: str | None = None
|
|
8
|
+
timeout: float | None = None
|
|
9
|
+
max_retries: int | None = None
|
|
10
|
+
default_headers: dict[str, str] | None = None
|
|
11
|
+
default_query: dict[str, object] | None = None
|
|
12
|
+
_client: object | None = None
|
|
13
|
+
provider_args: dict = field(default_factory=dict)
|
|
14
|
+
|
|
15
|
+
def __post_init__(self) -> None:
|
|
16
|
+
raise NotImplementedError("not yet implemented")
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def client(self):
|
|
20
|
+
raise NotImplementedError("not yet implemented")
|
|
21
|
+
|
|
22
|
+
def _client_kwargs(self) -> dict:
|
|
23
|
+
'''Keyword arguments for the SDK client: the typed fields that are set, overridden by provider_args.'''
|
|
24
|
+
typed_args = {
|
|
25
|
+
"api_key": self.api_key,
|
|
26
|
+
"base_url": self.base_url,
|
|
27
|
+
"timeout": self.timeout,
|
|
28
|
+
"max_retries": self.max_retries,
|
|
29
|
+
"default_headers": self.default_headers,
|
|
30
|
+
"default_query": self.default_query,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
**{key: value for key, value in typed_args.items() if value is not None},
|
|
35
|
+
**self.provider_args,
|
|
36
|
+
}
|