langchain-b12 0.1.8__py3-none-any.whl → 0.1.10__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.
@@ -7,10 +7,6 @@ from typing import Any, Literal, cast
7
7
  from google import genai
8
8
  from google.genai import types
9
9
  from google.oauth2 import service_account
10
- from langchain_b12.genai.genai_utils import (
11
- convert_messages_to_contents,
12
- parse_response_candidate,
13
- )
14
10
  from langchain_core.callbacks import (
15
11
  AsyncCallbackManagerForLLMRun,
16
12
  CallbackManagerForLLMRun,
@@ -40,6 +36,18 @@ from langchain_core.utils.function_calling import (
40
36
  convert_to_openai_tool,
41
37
  )
42
38
  from pydantic import BaseModel, ConfigDict, Field
39
+ from tenacity import (
40
+ retry,
41
+ retry_if_exception_type,
42
+ stop_after_attempt,
43
+ stop_never,
44
+ wait_exponential_jitter,
45
+ )
46
+
47
+ from langchain_b12.genai.genai_utils import (
48
+ convert_messages_to_contents,
49
+ parse_response_candidate,
50
+ )
43
51
 
44
52
  logger = logging.getLogger(__name__)
45
53
 
@@ -76,7 +84,7 @@ class ChatGenAI(BaseChatModel):
76
84
  seed: int | None = None
77
85
  """Random seed for the generation."""
78
86
  max_retries: int | None = Field(default=3)
79
- """Maximum number of retries when generation fails. None disables retries."""
87
+ """Maximum number of retries when generation fails. None retries indefinitely."""
80
88
  safety_settings: list[types.SafetySetting] | None = None
81
89
  """The default safety settings to use for all generations.
82
90
 
@@ -175,24 +183,10 @@ class ChatGenAI(BaseChatModel):
175
183
  run_manager: CallbackManagerForLLMRun | None = None,
176
184
  **kwargs: Any,
177
185
  ) -> ChatResult:
178
- attempts = 0
179
- while True:
180
- try:
181
- stream_iter = self._stream(
182
- messages, stop=stop, run_manager=run_manager, **kwargs
183
- )
184
- return generate_from_stream(stream_iter)
185
- except Exception as e: # noqa: BLE001
186
- if self.max_retries is None or attempts >= self.max_retries:
187
- raise
188
- attempts += 1
189
- logger.warning(
190
- "ChatGenAI._generate failed (attempt %d/%d). "
191
- "Retrying... Error: %s",
192
- attempts,
193
- self.max_retries,
194
- e,
195
- )
186
+ stream_iter = self._stream(
187
+ messages, stop=stop, run_manager=run_manager, **kwargs
188
+ )
189
+ return generate_from_stream(stream_iter)
196
190
 
197
191
  async def _agenerate(
198
192
  self,
@@ -201,24 +195,10 @@ class ChatGenAI(BaseChatModel):
201
195
  run_manager: AsyncCallbackManagerForLLMRun | None = None,
202
196
  **kwargs: Any,
203
197
  ) -> ChatResult:
204
- attempts = 0
205
- while True:
206
- try:
207
- stream_iter = self._astream(
208
- messages, stop=stop, run_manager=run_manager, **kwargs
209
- )
210
- return await agenerate_from_stream(stream_iter)
211
- except Exception as e: # noqa: BLE001
212
- if self.max_retries is None or attempts >= self.max_retries:
213
- raise
214
- attempts += 1
215
- logger.warning(
216
- "ChatGenAI._agenerate failed (attempt %d/%d). "
217
- "Retrying... Error: %s",
218
- attempts,
219
- self.max_retries,
220
- e,
221
- )
198
+ stream_iter = self._astream(
199
+ messages, stop=stop, run_manager=run_manager, **kwargs
200
+ )
201
+ return await agenerate_from_stream(stream_iter)
222
202
 
223
203
  def _stream(
224
204
  self,
@@ -228,26 +208,64 @@ class ChatGenAI(BaseChatModel):
228
208
  **kwargs: Any,
229
209
  ) -> Iterator[ChatGenerationChunk]:
230
210
  system_message, contents = self._prepare_request(messages=messages)
231
- response_iter = self.client.models.generate_content_stream(
232
- model=self.model_name,
233
- contents=contents,
234
- config=types.GenerateContentConfig(
235
- system_instruction=system_message,
236
- temperature=self.temperature,
237
- top_k=self.top_k,
238
- top_p=self.top_p,
239
- max_output_tokens=self.max_output_tokens,
240
- candidate_count=self.n,
241
- stop_sequences=stop or self.stop,
242
- safety_settings=self.safety_settings,
243
- thinking_config=self.thinking_config,
244
- automatic_function_calling=types.AutomaticFunctionCallingConfig(
245
- disable=True,
246
- ),
247
- **kwargs,
211
+
212
+ @retry(
213
+ reraise=True,
214
+ stop=stop_after_attempt(self.max_retries + 1)
215
+ if self.max_retries is not None
216
+ else stop_never,
217
+ wait=wait_exponential_jitter(initial=1, max=60),
218
+ retry=retry_if_exception_type(Exception),
219
+ before_sleep=lambda retry_state: logger.warning(
220
+ "ChatGenAI._stream failed to start (attempt %d/%s). "
221
+ "Retrying in %.2fs... Error: %s",
222
+ retry_state.attempt_number,
223
+ self.max_retries + 1 if self.max_retries is not None else "∞",
224
+ retry_state.next_action.sleep,
225
+ retry_state.outcome.exception(),
248
226
  ),
249
227
  )
250
- total_lc_usage = None
228
+ def _initiate_stream() -> tuple[
229
+ ChatGenerationChunk,
230
+ Iterator[types.GenerateContentResponse],
231
+ UsageMetadata | None,
232
+ ]:
233
+ """Initialize stream and fetch first chunk. Retries only apply here."""
234
+ response_iter = self.client.models.generate_content_stream(
235
+ model=self.model_name,
236
+ contents=contents,
237
+ config=types.GenerateContentConfig(
238
+ system_instruction=system_message,
239
+ temperature=self.temperature,
240
+ top_k=self.top_k,
241
+ top_p=self.top_p,
242
+ max_output_tokens=self.max_output_tokens,
243
+ candidate_count=self.n,
244
+ stop_sequences=stop or self.stop,
245
+ safety_settings=self.safety_settings,
246
+ thinking_config=self.thinking_config,
247
+ automatic_function_calling=types.AutomaticFunctionCallingConfig(
248
+ disable=True,
249
+ ),
250
+ **kwargs,
251
+ ),
252
+ )
253
+ # Fetch first chunk to ensure connection is established
254
+ first_response = next(iter(response_iter))
255
+ first_chunk, total_usage = self._gemini_chunk_to_generation_chunk(
256
+ first_response, prev_total_usage=None
257
+ )
258
+ return first_chunk, response_iter, total_usage
259
+
260
+ # Retry only covers stream initialization and first chunk
261
+ first_chunk, response_iter, total_lc_usage = _initiate_stream()
262
+
263
+ # Yield first chunk
264
+ if run_manager and isinstance(first_chunk.message.content, str):
265
+ run_manager.on_llm_new_token(first_chunk.message.content)
266
+ yield first_chunk
267
+
268
+ # Continue streaming without retry (retries during streaming are not well defined)
251
269
  for response_chunk in response_iter:
252
270
  chunk, total_lc_usage = self._gemini_chunk_to_generation_chunk(
253
271
  response_chunk, prev_total_usage=total_lc_usage
@@ -264,27 +282,65 @@ class ChatGenAI(BaseChatModel):
264
282
  **kwargs: Any,
265
283
  ) -> AsyncIterator[ChatGenerationChunk]:
266
284
  system_message, contents = self._prepare_request(messages=messages)
267
- response_iter = self.client.aio.models.generate_content_stream(
268
- model=self.model_name,
269
- contents=contents,
270
- config=types.GenerateContentConfig(
271
- system_instruction=system_message,
272
- temperature=self.temperature,
273
- top_k=self.top_k,
274
- top_p=self.top_p,
275
- max_output_tokens=self.max_output_tokens,
276
- candidate_count=self.n,
277
- stop_sequences=stop or self.stop,
278
- safety_settings=self.safety_settings,
279
- thinking_config=self.thinking_config,
280
- automatic_function_calling=types.AutomaticFunctionCallingConfig(
281
- disable=True,
282
- ),
283
- **kwargs,
285
+
286
+ @retry(
287
+ reraise=True,
288
+ stop=stop_after_attempt(self.max_retries + 1)
289
+ if self.max_retries is not None
290
+ else stop_never,
291
+ wait=wait_exponential_jitter(initial=1, max=60),
292
+ retry=retry_if_exception_type(Exception),
293
+ before_sleep=lambda retry_state: logger.warning(
294
+ "ChatGenAI._astream failed to start (attempt %d/%s). "
295
+ "Retrying in %.2fs... Error: %s",
296
+ retry_state.attempt_number,
297
+ self.max_retries + 1 if self.max_retries is not None else "∞",
298
+ retry_state.next_action.sleep,
299
+ retry_state.outcome.exception(),
284
300
  ),
285
301
  )
286
- total_lc_usage = None
287
- async for response_chunk in await response_iter:
302
+ async def _initiate_stream() -> tuple[
303
+ ChatGenerationChunk,
304
+ AsyncIterator[types.GenerateContentResponse],
305
+ UsageMetadata | None,
306
+ ]:
307
+ """Initialize stream and fetch first chunk. Retries only apply here."""
308
+ response_iter = await self.client.aio.models.generate_content_stream(
309
+ model=self.model_name,
310
+ contents=contents,
311
+ config=types.GenerateContentConfig(
312
+ system_instruction=system_message,
313
+ temperature=self.temperature,
314
+ top_k=self.top_k,
315
+ top_p=self.top_p,
316
+ max_output_tokens=self.max_output_tokens,
317
+ candidate_count=self.n,
318
+ stop_sequences=stop or self.stop,
319
+ safety_settings=self.safety_settings,
320
+ thinking_config=self.thinking_config,
321
+ automatic_function_calling=types.AutomaticFunctionCallingConfig(
322
+ disable=True,
323
+ ),
324
+ **kwargs,
325
+ ),
326
+ )
327
+ # Fetch first chunk to ensure connection is established
328
+ first_response = await response_iter.__anext__()
329
+ first_chunk, total_usage = self._gemini_chunk_to_generation_chunk(
330
+ first_response, prev_total_usage=None
331
+ )
332
+ return first_chunk, response_iter, total_usage
333
+
334
+ # Retry only covers stream initialization and first chunk
335
+ first_chunk, response_iter, total_lc_usage = await _initiate_stream()
336
+
337
+ # Yield first chunk
338
+ if run_manager and isinstance(first_chunk.message.content, str):
339
+ await run_manager.on_llm_new_token(first_chunk.message.content)
340
+ yield first_chunk
341
+
342
+ # Continue streaming without retry (retries during streaming are not well defined)
343
+ async for response_chunk in response_iter:
288
344
  chunk, total_lc_usage = self._gemini_chunk_to_generation_chunk(
289
345
  response_chunk, prev_total_usage=total_lc_usage
290
346
  )
@@ -1,10 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langchain-b12
3
- Version: 0.1.8
3
+ Version: 0.1.10
4
4
  Summary: A reusable collection of tools and implementations for Langchain
5
5
  Author-email: Vincent Min <vincent.min@b12-consulting.com>
6
6
  Requires-Python: >=3.11
7
7
  Requires-Dist: langchain-core>=0.3.60
8
+ Requires-Dist: pytest-anyio>=0.0.0
9
+ Requires-Dist: tenacity>=9.1.2
8
10
  Description-Content-Type: text/markdown
9
11
 
10
12
  # Langchain B12
@@ -2,8 +2,8 @@ langchain_b12/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  langchain_b12/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  langchain_b12/citations/citations.py,sha256=ZQvYayjQXIUaRosJ0qwL3Nc7kC8sBzmaIkE-BOslaVI,12261
4
4
  langchain_b12/genai/embeddings.py,sha256=h0Z-5PltDW9q79AjSrLemsz-_QKMB-043XXDvYSRQds,3483
5
- langchain_b12/genai/genai.py,sha256=4Q0j2YsAPLrHhIy_pYXoncb4hqJJkEUpxB3oD3qaECI,18120
5
+ langchain_b12/genai/genai.py,sha256=JoivVvUBl-mvRpl9UTC_Q6-8N4DkEHK1f7-bqI_V6Y4,20786
6
6
  langchain_b12/genai/genai_utils.py,sha256=tA6UiJURK25-11vtaX4768UV47jDCYwVKIIWydD4Egw,10736
7
- langchain_b12-0.1.8.dist-info/METADATA,sha256=0-KZr-PXjE16ar4LpQbdWHX8CrViLBxlfV9uGwE0Qw0,1204
8
- langchain_b12-0.1.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
- langchain_b12-0.1.8.dist-info/RECORD,,
7
+ langchain_b12-0.1.10.dist-info/METADATA,sha256=LcMlsuxt4CO9Q-FeGqR3tx2mhmEhEMAagkWvBTmUtbo,1271
8
+ langchain_b12-0.1.10.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
9
+ langchain_b12-0.1.10.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any