agent-framework-typesafe 1.0.0a261002__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.
@@ -0,0 +1,17 @@
1
+ # Copyright (c) Microsoft. All rights reserved.
2
+
3
+ import importlib.metadata
4
+
5
+ from ._chat_client import RawTypeSafeChatClient, TypeSafeChatClient, TypeSafeChatOptions
6
+
7
+ try:
8
+ __version__ = importlib.metadata.version(__name__)
9
+ except importlib.metadata.PackageNotFoundError:
10
+ __version__ = "0.0.0"
11
+
12
+ __all__ = [
13
+ "RawTypeSafeChatClient",
14
+ "TypeSafeChatClient",
15
+ "TypeSafeChatOptions",
16
+ "__version__",
17
+ ]
@@ -0,0 +1,681 @@
1
+ # Copyright (c) Microsoft. All rights reserved.
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Awaitable, Mapping, Sequence
7
+ from types import TracebackType
8
+ from typing import Any, ClassVar, Literal, NoReturn, cast, overload
9
+ from uuid import uuid4
10
+
11
+ import httpx2
12
+ from agent_framework import (
13
+ BaseChatClient,
14
+ ChatAndFunctionMiddlewareTypes,
15
+ ChatMiddlewareLayer,
16
+ ChatOptions,
17
+ ChatResponse,
18
+ ChatResponseUpdate,
19
+ CompactionStrategy,
20
+ Content,
21
+ FunctionInvocationConfiguration,
22
+ FunctionInvocationLayer,
23
+ FunctionTool,
24
+ Message,
25
+ ResponseStream,
26
+ TokenizerProtocol,
27
+ UsageDetails,
28
+ validate_tool_mode,
29
+ )
30
+ from agent_framework._settings import SecretString, load_settings
31
+ from agent_framework._telemetry import get_user_agent
32
+ from agent_framework.exceptions import (
33
+ ChatClientException,
34
+ ChatClientInvalidAuthException,
35
+ ChatClientInvalidRequestException,
36
+ ChatClientInvalidResponseException,
37
+ )
38
+ from agent_framework.observability import ChatTelemetryLayer
39
+ from typesafe_sdk import (
40
+ AsyncTypeSafeClient,
41
+ ChoiceAnswer,
42
+ Questions,
43
+ ScoreAnswer,
44
+ SystemOneResponse,
45
+ TypeSafeAPIConnectionError,
46
+ TypeSafeAPIError,
47
+ TypeSafeAPIResponseValidationError,
48
+ TypeSafeAuthenticationError,
49
+ TypeSafeBadRequestError,
50
+ TypeSafeError,
51
+ TypeSafeNotFoundError,
52
+ TypeSafePermissionDeniedError,
53
+ TypeSafeUnprocessableEntityError,
54
+ )
55
+ from typing_extensions import Self, TypedDict, override
56
+
57
+ from ._tool_calls import compile_tool_call_plan
58
+
59
+ _TYPESAFE_DEFAULT_BASE_URL = "https://api.typesafe.ai"
60
+ _TYPESAFE_SYSTEM_ONE_PATH = "/v1/systemone"
61
+
62
+
63
+ class _ApiKeyTransport(httpx2.AsyncBaseTransport):
64
+ """Restore bearer authentication after the TypeSafe SDK prepares redacted wire headers."""
65
+
66
+ def __init__(self, api_key: str) -> None:
67
+ self._api_key = api_key
68
+ self._transport = httpx2.AsyncHTTPTransport()
69
+
70
+ @override
71
+ async def handle_async_request(self, request: httpx2.Request) -> httpx2.Response:
72
+ request.headers["Authorization"] = f"Bearer {self._api_key}"
73
+ return await self._transport.handle_async_request(request)
74
+
75
+ @override
76
+ async def aclose(self) -> None:
77
+ await self._transport.aclose()
78
+
79
+
80
+ class TypeSafeSettings(TypedDict, total=False):
81
+ """TypeSafe settings resolved from explicit values, .env files, or the environment."""
82
+
83
+ api_key: SecretString | None
84
+ default_model: str | None
85
+ base_url: str | None
86
+
87
+
88
+ class TypeSafeChatOptions(ChatOptions[SystemOneResponse], total=False):
89
+ """TypeSafe-specific chat options.
90
+
91
+ Keys:
92
+ response_format: TypeSafe Questions mapping. The connector forwards it as
93
+ the TypeSafe questions parameter and returns SystemOneResponse. It may
94
+ be omitted when the client was created with default_questions.
95
+ model: Optional TypeSafe model override.
96
+ instructions: Optional Agent instructions included in the structured state.
97
+ """
98
+
99
+ pass
100
+
101
+
102
+ class RawTypeSafeChatClient(BaseChatClient[TypeSafeChatOptions]):
103
+ """Raw Agent Framework chat client for TypeSafe AI System One models.
104
+
105
+ The client maps text messages and Agent instructions to TypeSafe structured
106
+ state. The response_format option supplies the TypeSafe Questions mapping,
107
+ while every response is returned as SystemOneResponse. The raw client can
108
+ emit constrained function calls but does not execute them; TypeSafeChatClient
109
+ adds the function-invocation layer. Free-form generation, streaming, and
110
+ non-text message content are not supported.
111
+
112
+ Use TypeSafeChatClient for the standard middleware and telemetry layers.
113
+ """
114
+
115
+ OTEL_PROVIDER_NAME: ClassVar[str] = "typesafe.ai"
116
+ _SUPPORTED_OPTIONS: ClassVar[frozenset[str]] = frozenset({
117
+ "allow_multiple_tool_calls",
118
+ "instructions",
119
+ "model",
120
+ "response_format",
121
+ "tool_choice",
122
+ "tools",
123
+ })
124
+
125
+ def __init__(
126
+ self,
127
+ *,
128
+ api_key: str | SecretString | None = None,
129
+ model: str | None = None,
130
+ base_url: str | None = None,
131
+ async_client: AsyncTypeSafeClient | None = None,
132
+ default_questions: Questions | None = None,
133
+ compaction_strategy: CompactionStrategy | None = None,
134
+ tokenizer: TokenizerProtocol | None = None,
135
+ additional_properties: dict[str, Any] | None = None,
136
+ env_file_path: str | None = None,
137
+ env_file_encoding: str | None = None,
138
+ ) -> None:
139
+ """Create a raw TypeSafe AI chat client.
140
+
141
+ Keyword Args:
142
+ api_key: TypeSafe API key. Defaults to the TYPESAFE_API_KEY environment variable.
143
+ model: Default TypeSafe model. The SDK defaults to jev-latest.
144
+ base_url: Optional TypeSafe API root. Defaults to TYPESAFE_BASE_URL
145
+ or https://api.typesafe.ai for connector-owned SDK clients.
146
+ async_client: Optional preconfigured TypeSafe SDK client. It remains caller-owned.
147
+ default_questions: Questions used when a request omits response_format.
148
+ This is useful for framework integrations that invoke a chat client
149
+ with a fixed task contract, such as SecureAgentConfig quarantine calls.
150
+ compaction_strategy: Optional compaction strategy applied before requests.
151
+ tokenizer: Optional tokenizer used by token-aware compaction strategies.
152
+ additional_properties: Additional properties stored on the client.
153
+ env_file_path: Path to a .env file used for settings resolution.
154
+ env_file_encoding: Encoding used to read the .env file.
155
+ """
156
+ self._owns_client = async_client is None
157
+ self.default_questions = (
158
+ self._validate_questions_mapping(default_questions, setting_name="default_questions")
159
+ if default_questions is not None
160
+ else None
161
+ )
162
+
163
+ if async_client is not None:
164
+ self.client = async_client
165
+ self.model = model
166
+ self.base_url = None
167
+ self._service_url = "Unknown"
168
+ else:
169
+ settings = load_settings(
170
+ TypeSafeSettings,
171
+ env_prefix="TYPESAFE_",
172
+ required_fields=["api_key"],
173
+ api_key=api_key,
174
+ default_model=model,
175
+ base_url=base_url,
176
+ env_file_path=env_file_path,
177
+ env_file_encoding=env_file_encoding,
178
+ )
179
+ self.model = settings.get("default_model")
180
+ self.base_url = settings.get("base_url") or _TYPESAFE_DEFAULT_BASE_URL
181
+ self._service_url = f"{self.base_url.rstrip('/')}{_TYPESAFE_SYSTEM_ONE_PATH}"
182
+ api_key_secret = cast(SecretString, settings.get("api_key"))
183
+ api_key_value = api_key_secret.get_secret_value()
184
+ self.client = AsyncTypeSafeClient(
185
+ api_key=api_key_value,
186
+ model=self.model,
187
+ base_url=self.base_url,
188
+ headers={"User-Agent": get_user_agent()},
189
+ transport=_ApiKeyTransport(api_key_value),
190
+ )
191
+ super().__init__(
192
+ compaction_strategy=compaction_strategy,
193
+ tokenizer=tokenizer,
194
+ additional_properties=additional_properties,
195
+ )
196
+
197
+ async def __aenter__(self) -> Self:
198
+ """Enter the client context."""
199
+ return self
200
+
201
+ async def __aexit__(
202
+ self,
203
+ exc_type: type[BaseException] | None,
204
+ exc_value: BaseException | None,
205
+ traceback: TracebackType | None,
206
+ ) -> None:
207
+ """Close connector-owned resources."""
208
+ await self.close()
209
+
210
+ async def close(self) -> None:
211
+ """Close the internally created TypeSafe SDK client."""
212
+ if self._owns_client:
213
+ await self.client.aclose()
214
+
215
+ @override
216
+ def service_url(self) -> str:
217
+ """Return the TypeSafe System One endpoint."""
218
+ return self._service_url
219
+
220
+ @override
221
+ def _inner_get_response(
222
+ self,
223
+ *,
224
+ messages: Sequence[Message],
225
+ stream: bool,
226
+ options: Mapping[str, Any],
227
+ **kwargs: Any,
228
+ ) -> Awaitable[ChatResponse[Any]] | ResponseStream[ChatResponseUpdate, ChatResponse[Any]]:
229
+ """Evaluate messages with TypeSafe and return structured answers."""
230
+ if stream:
231
+ raise ChatClientInvalidRequestException("TypeSafe System One does not support streaming responses.")
232
+ unexpected_kwargs = set(kwargs) - {"tool_choice"}
233
+ if unexpected_kwargs:
234
+ raise ChatClientInvalidRequestException(
235
+ f"TypeSafe does not support client-specific arguments: {', '.join(sorted(unexpected_kwargs))}."
236
+ )
237
+ request_options = dict(options)
238
+ if "tool_choice" in kwargs:
239
+ request_options.setdefault("tool_choice", kwargs["tool_choice"])
240
+
241
+ async def _get_response() -> ChatResponse[Any]:
242
+ normalized_options = await self._validate_options(request_options)
243
+ self._validate_supported_options(normalized_options)
244
+
245
+ model = normalized_options.get("model", self.model)
246
+ if model is not None and not isinstance(model, str):
247
+ raise ChatClientInvalidRequestException("TypeSafe model must be a string.")
248
+
249
+ user_questions = self._get_questions(normalized_options)
250
+ tools = self._get_function_tools(normalized_options)
251
+ tool_mode = validate_tool_mode(normalized_options.get("tool_choice"))
252
+ tool_plan = compile_tool_call_plan(
253
+ tools,
254
+ tool_mode=tool_mode,
255
+ user_question_ids=set(user_questions),
256
+ previous_calls=self._get_current_turn_function_calls(messages),
257
+ )
258
+ questions = dict(user_questions)
259
+ if tool_plan is not None:
260
+ questions.update(tool_plan.questions)
261
+
262
+ state = self._build_state(
263
+ messages,
264
+ instructions=normalized_options.get("instructions"),
265
+ )
266
+
267
+ try:
268
+ response = await self.client.system_one( # pyright: ignore[reportUnknownMemberType]
269
+ state=state,
270
+ questions=questions,
271
+ model=model,
272
+ response_model=SystemOneResponse,
273
+ )
274
+ except TypeSafeError as exc:
275
+ self._raise_sdk_error(exc)
276
+ except Exception as exc:
277
+ raise ChatClientException(
278
+ f"TypeSafe request failed: {exc}",
279
+ inner_exception=exc,
280
+ ) from exc
281
+
282
+ if not isinstance(response, SystemOneResponse):
283
+ raise ChatClientInvalidResponseException(
284
+ "TypeSafe returned a response that does not match SystemOneResponse."
285
+ )
286
+
287
+ input_tokens = response.usage.input_tokens
288
+ output_tokens = response.usage.output_tokens
289
+ usage_details = self._build_usage_details(input_tokens, output_tokens)
290
+
291
+ if tool_plan is not None and (tool_call := tool_plan.decode(response)) is not None:
292
+ function, arguments = tool_call
293
+ return ChatResponse(
294
+ messages=[
295
+ Message(
296
+ role="assistant",
297
+ contents=[
298
+ Content.from_function_call(
299
+ call_id=f"typesafe-{uuid4().hex}",
300
+ name=function.name,
301
+ arguments=arguments,
302
+ )
303
+ ],
304
+ )
305
+ ],
306
+ response_id=response.request_id,
307
+ model=response.model,
308
+ finish_reason="tool_calls",
309
+ usage_details=usage_details or None,
310
+ )
311
+
312
+ response = self._filter_internal_answers(response, set(user_questions))
313
+ terminal_text = self._build_terminal_text(messages, response)
314
+ return ChatResponse(
315
+ messages=[Message(role="assistant", contents=[terminal_text])],
316
+ response_id=response.request_id,
317
+ model=response.model,
318
+ finish_reason="stop",
319
+ usage_details=usage_details or None,
320
+ value=response,
321
+ response_format=SystemOneResponse,
322
+ raw_representation=response,
323
+ )
324
+
325
+ return _get_response()
326
+
327
+ @override
328
+ async def _validate_options(self, options: Mapping[str, Any]) -> dict[str, Any]:
329
+ raw_options = dict(options)
330
+ raw_tools = raw_options.pop("tools", None)
331
+ validated = await super()._validate_options(raw_options)
332
+ if raw_tools is None:
333
+ return validated
334
+ tool_items: list[Any] = (
335
+ list(cast(Sequence[Any], raw_tools))
336
+ if isinstance(raw_tools, Sequence) and not isinstance(raw_tools, (str, bytes))
337
+ else [raw_tools]
338
+ )
339
+ if not all(isinstance(tool, FunctionTool) for tool in tool_items):
340
+ raise ChatClientInvalidRequestException(
341
+ "TypeSafe raw tool routing requires FunctionTool instances. "
342
+ "Pass MCPTool objects through Agent.run so Agent Framework can connect and expand them first."
343
+ )
344
+ validated["tools"] = list(tool_items)
345
+ return validated
346
+
347
+ @classmethod
348
+ def _validate_supported_options(cls, options: Mapping[str, Any]) -> None:
349
+ unsupported = sorted(
350
+ key for key, value in options.items() if key not in cls._SUPPORTED_OPTIONS and value is not None
351
+ )
352
+ if unsupported:
353
+ raise ChatClientInvalidRequestException(
354
+ "TypeSafe does not support these chat options: "
355
+ f"{', '.join(unsupported)}. Use response_format to define structured judgments."
356
+ )
357
+
358
+ if options.get("allow_multiple_tool_calls"):
359
+ raise ChatClientInvalidRequestException("TypeSafe supports one tool call per agent run.")
360
+
361
+ def _get_questions(self, options: Mapping[str, Any]) -> Questions:
362
+ response_format = options.get("response_format")
363
+ if response_format is None and self.default_questions is not None:
364
+ return self.default_questions
365
+ return self._validate_questions_mapping(response_format, setting_name="response_format")
366
+
367
+ @staticmethod
368
+ def _validate_questions_mapping(value: Any, *, setting_name: str) -> Questions:
369
+ if not isinstance(value, Mapping) or not value:
370
+ raise ChatClientInvalidRequestException(
371
+ f"TypeSafe {setting_name} must be a non-empty typesafe_sdk.Questions mapping."
372
+ )
373
+ question_ids: list[Any] = list(cast(Mapping[Any, Any], value))
374
+ invalid_question_ids = [question_id for question_id in question_ids if not isinstance(question_id, str)]
375
+ if invalid_question_ids:
376
+ raise ChatClientInvalidRequestException(f"TypeSafe {setting_name} question IDs must be strings.")
377
+ return cast(Questions, value)
378
+
379
+ @staticmethod
380
+ def _get_function_tools(options: Mapping[str, Any]) -> list[FunctionTool]:
381
+ tools = options.get("tools")
382
+ if tools is None:
383
+ return []
384
+ if isinstance(tools, Sequence) and not isinstance(tools, (str, bytes)):
385
+ return [cast(FunctionTool, tool) for tool in cast(Sequence[Any], tools)]
386
+ return [cast(FunctionTool, tools)]
387
+
388
+ @staticmethod
389
+ def _get_current_turn_function_calls(messages: Sequence[Message]) -> dict[str, list[dict[str, Any]]]:
390
+ calls: dict[str, list[dict[str, Any]]] = {}
391
+ for message in reversed(messages):
392
+ if message.role == "user":
393
+ break
394
+ for content in reversed(message.contents):
395
+ if content.type == "function_call" and content.name:
396
+ calls.setdefault(content.name, []).append(content.parse_arguments() or {})
397
+ for function_calls in calls.values():
398
+ function_calls.reverse()
399
+ return calls
400
+
401
+ @staticmethod
402
+ def _build_state(messages: Sequence[Message], *, instructions: Any) -> dict[str, Any]:
403
+ if instructions is not None and not isinstance(instructions, str):
404
+ raise ChatClientInvalidRequestException("TypeSafe instructions must be a string.")
405
+
406
+ state_messages: list[dict[str, Any]] = []
407
+ for index, message in enumerate(messages):
408
+ unsupported_content_types = sorted({
409
+ content.type
410
+ for content in message.contents
411
+ if content.type not in {"text", "text_reasoning", "function_call", "function_result"}
412
+ })
413
+ if unsupported_content_types:
414
+ raise ChatClientInvalidRequestException(
415
+ f"TypeSafe only supports text, reasoning summaries, and function call/result content; "
416
+ f"message {index} contains: "
417
+ f"{', '.join(unsupported_content_types)}."
418
+ )
419
+
420
+ contents: list[dict[str, Any]] = []
421
+ for content in message.contents:
422
+ if content.type == "text" and content.text:
423
+ contents.append({"type": "text", "text": content.text})
424
+ elif content.type == "text_reasoning" and content.text:
425
+ contents.append({"type": "text_reasoning", "text": content.text})
426
+ elif content.type == "function_call":
427
+ contents.append({
428
+ "type": "function_call",
429
+ "call_id": content.call_id,
430
+ "name": content.name,
431
+ "arguments": content.parse_arguments(),
432
+ })
433
+ elif content.type == "function_result":
434
+ contents.append(RawTypeSafeChatClient._serialize_function_result(content))
435
+ if contents:
436
+ state_messages.append({
437
+ "role": str(message.role),
438
+ "contents": contents,
439
+ })
440
+
441
+ if not state_messages:
442
+ raise ChatClientInvalidRequestException("TypeSafe requires at least one non-empty text message.")
443
+
444
+ state: dict[str, Any] = {"messages": state_messages}
445
+ if instructions:
446
+ state["instructions"] = instructions
447
+ return state
448
+
449
+ @staticmethod
450
+ def _serialize_function_result(content: Content) -> dict[str, Any]:
451
+ items: list[dict[str, str]] = []
452
+ for item in content.items or ():
453
+ if item.type != "text":
454
+ raise ChatClientInvalidRequestException(
455
+ f"TypeSafe function results support text items only; received unsupported {item.type!r} content."
456
+ )
457
+ items.append({"type": "text", "text": item.text or ""})
458
+ return {
459
+ "type": "function_result",
460
+ "call_id": content.call_id,
461
+ "result": content.result,
462
+ **({"items": items} if items else {}),
463
+ }
464
+
465
+ @staticmethod
466
+ def _build_usage_details(input_tokens: int | None, output_tokens: int | None) -> UsageDetails:
467
+ return UsageDetails(
468
+ **({"input_token_count": input_tokens} if input_tokens is not None else {}),
469
+ **({"output_token_count": output_tokens} if output_tokens is not None else {}),
470
+ **(
471
+ {"total_token_count": input_tokens + output_tokens}
472
+ if input_tokens is not None and output_tokens is not None
473
+ else {}
474
+ ),
475
+ )
476
+
477
+ @staticmethod
478
+ def _build_terminal_text(messages: Sequence[Message], response: SystemOneResponse) -> str:
479
+ tool_results = RawTypeSafeChatClient._get_current_turn_function_result_texts(messages)
480
+ if not tool_results:
481
+ return response.model_dump_json()
482
+
483
+ decision_lines = [
484
+ f"{question_id}: {answer.choice}"
485
+ if isinstance(answer, ChoiceAnswer)
486
+ else f"{question_id}: {answer.score:g}"
487
+ for question_id, answer in response.answers.items()
488
+ if isinstance(answer, (ChoiceAnswer, ScoreAnswer))
489
+ ]
490
+ return "\n".join([*tool_results, *decision_lines])
491
+
492
+ @staticmethod
493
+ def _get_current_turn_function_result_texts(messages: Sequence[Message]) -> list[str]:
494
+ results: list[str] = []
495
+ for message in reversed(messages):
496
+ if message.role == "user":
497
+ break
498
+ for content in reversed(message.contents):
499
+ if content.type == "function_result" and isinstance(content.result, str):
500
+ results.append(RawTypeSafeChatClient._unwrap_function_result_text(content.result))
501
+ results.reverse()
502
+ return results
503
+
504
+ @staticmethod
505
+ def _unwrap_function_result_text(result: str) -> str:
506
+ try:
507
+ decoded: Any = json.loads(result)
508
+ except json.JSONDecodeError:
509
+ return result
510
+ if isinstance(decoded, dict):
511
+ decoded_result = cast(dict[str, Any], decoded)
512
+ if set(decoded_result) == {"result"} and isinstance(decoded_result["result"], str):
513
+ return decoded_result["result"]
514
+ return result
515
+
516
+ @staticmethod
517
+ def _filter_internal_answers(response: SystemOneResponse, question_ids: set[str]) -> SystemOneResponse:
518
+ missing = sorted(question_id for question_id in question_ids if question_id not in response.answers)
519
+ if missing:
520
+ raise ChatClientInvalidResponseException(
521
+ f"TypeSafe response is missing configured answers: {', '.join(missing)}."
522
+ )
523
+ if set(response.answers) == question_ids:
524
+ return response
525
+ filtered = response.model_copy(
526
+ update={"answers": {question_id: response.answers[question_id] for question_id in question_ids}}
527
+ )
528
+ filtered.__dict__.pop("_raw", None) # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType]
529
+ return filtered
530
+
531
+ @staticmethod
532
+ def _raise_sdk_error(exc: TypeSafeError) -> NoReturn:
533
+ if isinstance(exc, TypeSafeAPIResponseValidationError):
534
+ raise ChatClientInvalidResponseException(
535
+ f"TypeSafe returned an invalid response: {exc}",
536
+ inner_exception=exc,
537
+ ) from exc
538
+ if isinstance(exc, (TypeSafeAuthenticationError, TypeSafePermissionDeniedError)):
539
+ raise ChatClientInvalidAuthException(
540
+ f"TypeSafe authentication failed: {exc}",
541
+ inner_exception=exc,
542
+ ) from exc
543
+ if isinstance(exc, (TypeSafeBadRequestError, TypeSafeNotFoundError, TypeSafeUnprocessableEntityError)):
544
+ raise ChatClientInvalidRequestException(
545
+ f"Invalid TypeSafe request: {exc}",
546
+ inner_exception=exc,
547
+ ) from exc
548
+ if isinstance(exc, (TypeSafeAPIConnectionError, TypeSafeAPIError)):
549
+ raise ChatClientException(
550
+ f"TypeSafe request failed: {exc}",
551
+ inner_exception=exc,
552
+ ) from exc
553
+ raise ChatClientInvalidRequestException(
554
+ f"Invalid TypeSafe request: {exc}",
555
+ inner_exception=exc,
556
+ ) from exc
557
+
558
+
559
+ class TypeSafeChatClient(
560
+ FunctionInvocationLayer[TypeSafeChatOptions],
561
+ ChatMiddlewareLayer[TypeSafeChatOptions],
562
+ ChatTelemetryLayer[TypeSafeChatOptions],
563
+ RawTypeSafeChatClient,
564
+ ):
565
+ """TypeSafe AI chat client with function invocation, middleware, and telemetry support.
566
+
567
+ This is the recommended client for most uses. Use RawTypeSafeChatClient
568
+ when composing a custom layer stack or opting out of telemetry.
569
+ """
570
+
571
+ @overload
572
+ def get_response(
573
+ self,
574
+ messages: Sequence[Message],
575
+ *,
576
+ stream: Literal[False] = ...,
577
+ options: TypeSafeChatOptions | ChatOptions[Any] | None = None,
578
+ middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
579
+ compaction_strategy: CompactionStrategy | None = None,
580
+ tokenizer: TokenizerProtocol | None = None,
581
+ function_invocation_kwargs: Mapping[str, Any] | None = None,
582
+ client_kwargs: Mapping[str, Any] | None = None,
583
+ ) -> Awaitable[ChatResponse[Any]]: ...
584
+
585
+ @overload
586
+ def get_response(
587
+ self,
588
+ messages: Sequence[Message],
589
+ *,
590
+ stream: Literal[True],
591
+ options: TypeSafeChatOptions | ChatOptions[Any] | None = None,
592
+ middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
593
+ compaction_strategy: CompactionStrategy | None = None,
594
+ tokenizer: TokenizerProtocol | None = None,
595
+ function_invocation_kwargs: Mapping[str, Any] | None = None,
596
+ client_kwargs: Mapping[str, Any] | None = None,
597
+ ) -> ResponseStream[ChatResponseUpdate, ChatResponse[Any]]: ...
598
+
599
+ @override
600
+ def get_response(
601
+ self,
602
+ messages: Sequence[Message],
603
+ *,
604
+ stream: bool = False,
605
+ options: TypeSafeChatOptions | ChatOptions[Any] | None = None,
606
+ middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
607
+ compaction_strategy: CompactionStrategy | None = None,
608
+ tokenizer: TokenizerProtocol | None = None,
609
+ function_invocation_kwargs: Mapping[str, Any] | None = None,
610
+ client_kwargs: Mapping[str, Any] | None = None,
611
+ ) -> Awaitable[ChatResponse[Any]] | ResponseStream[ChatResponseUpdate, ChatResponse[Any]]:
612
+ """Reject streaming before the function-invocation layer can execute tools."""
613
+ if stream:
614
+
615
+ async def _reject_streaming() -> ResponseStream[ChatResponseUpdate, ChatResponse[Any]]: # ruff: ignore[unused-async]
616
+ raise ChatClientInvalidRequestException("TypeSafe System One does not support streaming responses.")
617
+
618
+ return ResponseStream.from_awaitable( # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
619
+ _reject_streaming()
620
+ )
621
+
622
+ return super().get_response(
623
+ messages,
624
+ stream=False,
625
+ options=options,
626
+ middleware=middleware,
627
+ compaction_strategy=compaction_strategy,
628
+ tokenizer=tokenizer,
629
+ function_invocation_kwargs=function_invocation_kwargs,
630
+ client_kwargs=client_kwargs,
631
+ )
632
+
633
+ def __init__(
634
+ self,
635
+ *,
636
+ api_key: str | SecretString | None = None,
637
+ model: str | None = None,
638
+ base_url: str | None = None,
639
+ async_client: AsyncTypeSafeClient | None = None,
640
+ default_questions: Questions | None = None,
641
+ middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
642
+ function_invocation_configuration: FunctionInvocationConfiguration | None = None,
643
+ compaction_strategy: CompactionStrategy | None = None,
644
+ tokenizer: TokenizerProtocol | None = None,
645
+ additional_properties: dict[str, Any] | None = None,
646
+ env_file_path: str | None = None,
647
+ env_file_encoding: str | None = None,
648
+ ) -> None:
649
+ """Create a layered TypeSafe AI chat client.
650
+
651
+ Keyword Args:
652
+ api_key: TypeSafe API key. Defaults to the TYPESAFE_API_KEY environment variable.
653
+ model: Default TypeSafe model. The SDK defaults to jev-latest.
654
+ base_url: Optional TypeSafe API root for connector-owned SDK clients.
655
+ async_client: Optional preconfigured TypeSafe SDK client. It remains caller-owned.
656
+ default_questions: Questions used when a request omits response_format.
657
+ middleware: Chat and function middleware to apply around requests and tool calls.
658
+ function_invocation_configuration: Function invocation settings. TypeSafe limits
659
+ each run to one executed tool call.
660
+ compaction_strategy: Optional compaction strategy applied before requests.
661
+ tokenizer: Optional tokenizer used by token-aware compaction strategies.
662
+ additional_properties: Additional properties stored on the client.
663
+ env_file_path: Path to a .env file used for settings resolution.
664
+ env_file_encoding: Encoding used to read the .env file.
665
+ """
666
+ invocation_configuration = dict(function_invocation_configuration or {})
667
+ invocation_configuration.setdefault("max_function_calls", 1)
668
+ super().__init__(
669
+ api_key=api_key,
670
+ model=model,
671
+ base_url=base_url,
672
+ async_client=async_client,
673
+ default_questions=default_questions,
674
+ middleware=middleware,
675
+ function_invocation_configuration=cast(FunctionInvocationConfiguration, invocation_configuration),
676
+ compaction_strategy=compaction_strategy,
677
+ tokenizer=tokenizer,
678
+ additional_properties=additional_properties,
679
+ env_file_path=env_file_path,
680
+ env_file_encoding=env_file_encoding,
681
+ )