|
27 | 27 | from abc import ABC |
28 | 28 | from collections.abc import AsyncGenerator, Generator, Mapping, Sequence |
29 | 29 | from functools import cached_property |
30 | | -from typing import Any, ClassVar, Literal, Self |
| 30 | +from typing import Any, ClassVar, Literal |
31 | 31 |
|
32 | 32 | from httpx import URL, Response |
33 | 33 | from langchain_core.callbacks import ( |
|
38 | 38 | from langchain_core.language_models.chat_models import BaseChatModel |
39 | 39 | from langchain_core.messages import BaseMessage |
40 | 40 | from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult |
41 | | -from pydantic import AliasChoices, BaseModel, ConfigDict, Field, model_validator |
| 41 | +from pydantic import ( |
| 42 | + AliasChoices, |
| 43 | + BaseModel, |
| 44 | + ConfigDict, |
| 45 | + Field, |
| 46 | + ValidationInfo, |
| 47 | + field_validator, |
| 48 | +) |
42 | 49 |
|
43 | 50 | from uipath.llm_client.httpx_client import ( |
44 | 51 | UiPathHttpxAsyncClient, |
|
49 | 56 | get_captured_response_headers, |
50 | 57 | set_captured_response_headers, |
51 | 58 | ) |
52 | | -from uipath_langchain_client._sampling import strip_disabled_sampling_kwargs |
53 | 59 | from uipath_langchain_client.settings import ( |
54 | 60 | UiPathAPIConfig, |
55 | 61 | UiPathBaseSettings, |
56 | 62 | get_default_client_settings, |
57 | 63 | ) |
58 | | -from uipath_langchain_client.utils import RetryConfig |
| 64 | +from uipath_langchain_client.utils import RetryConfig, strip_disabled_sampling_kwargs |
59 | 65 |
|
60 | 66 |
|
61 | 67 | class UiPathBaseLLMClient(BaseModel, ABC): |
@@ -112,10 +118,36 @@ class UiPathBaseLLMClient(BaseModel, ABC): |
112 | 118 | model_details: dict[str, Any] | None = Field( |
113 | 119 | default=None, |
114 | 120 | description="Per-model capability flags sourced from the discovery endpoint " |
115 | | - "(e.g. {'shouldSkipTemperature': True}). The factory forwards it; direct " |
116 | | - "instantiation lazy-resolves it from client_settings on first construction.", |
| 121 | + "(e.g. {'shouldSkipTemperature': True}). The factory forwards it; when absent, " |
| 122 | + "the field validator below eagerly resolves it from client_settings.", |
117 | 123 | ) |
118 | 124 |
|
| 125 | + @field_validator("model_details", mode="after") |
| 126 | + @classmethod |
| 127 | + def _resolve_model_details( |
| 128 | + cls, value: dict[str, Any] | None, info: ValidationInfo |
| 129 | + ) -> dict[str, Any]: |
| 130 | + # Fields validate in declaration order, so by the time this runs both |
| 131 | + # ``client_settings`` and ``model_name`` are already in ``info.data``. |
| 132 | + # Eager resolution here keeps direct instantiation and the factory |
| 133 | + # path consistent. ``get_available_models`` is class-cached inside |
| 134 | + # the settings layer, so at most one discovery HTTP call fires per |
| 135 | + # process regardless of how many chat/embedding models are built. |
| 136 | + if value is not None: |
| 137 | + return value |
| 138 | + settings = info.data.get("client_settings") |
| 139 | + model_name = info.data.get("model_name") |
| 140 | + if settings is None or not model_name: |
| 141 | + return {} |
| 142 | + try: |
| 143 | + model_info = settings.get_model_info( |
| 144 | + model_name, |
| 145 | + byo_connection_id=info.data.get("byo_connection_id"), |
| 146 | + ) |
| 147 | + return model_info.get("modelDetails") or {} |
| 148 | + except Exception: |
| 149 | + return {} |
| 150 | + |
119 | 151 | default_headers: Mapping[str, str] | None = Field( |
120 | 152 | default=None, |
121 | 153 | description="Caller-supplied request headers. Merged on top of `class_default_headers`; " |
@@ -148,25 +180,6 @@ class UiPathBaseLLMClient(BaseModel, ABC): |
148 | 180 | description="Logger for request/response logging", |
149 | 181 | ) |
150 | 182 |
|
151 | | - @model_validator(mode="after") |
152 | | - def _resolve_model_details(self) -> Self: |
153 | | - # Populate model_details eagerly so direct instantiation behaves the |
154 | | - # same as the factory path. get_available_models is class-cached inside |
155 | | - # the settings layer, so at most one discovery HTTP call fires per |
156 | | - # process regardless of how many chat/embedding models are built. |
157 | | - # Placed on UiPathBaseLLMClient (not just the chat subclass) because |
158 | | - # model_details is meaningful for embedding wrappers too. |
159 | | - if self.model_details is None: |
160 | | - try: |
161 | | - info = self.client_settings.get_model_info( |
162 | | - self.model_name, |
163 | | - byo_connection_id=self.byo_connection_id, |
164 | | - ) |
165 | | - self.model_details = info.get("modelDetails") or {} |
166 | | - except Exception: |
167 | | - self.model_details = {} |
168 | | - return self |
169 | | - |
170 | 183 | @cached_property |
171 | 184 | def uipath_sync_client(self) -> UiPathHttpxClient: |
172 | 185 | """Here we instantiate a synchronous HTTP client with the proper authentication pipeline, retry logic, logging etc.""" |
|
0 commit comments