botrun-flow-lang 6.2.61__py3-none-any.whl → 6.2.62__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.
@@ -8,7 +8,7 @@ Supports tool calling for LangGraph react agent compatibility.
8
8
  """
9
9
 
10
10
  import json
11
- from typing import Any, Dict, List, Optional, Tuple, Union
11
+ from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
12
12
 
13
13
  import httpx
14
14
 
@@ -18,12 +18,13 @@ from langchain_core.callbacks import CallbackManagerForLLMRun
18
18
  from langchain_core.language_models.chat_models import BaseChatModel
19
19
  from langchain_core.messages import (
20
20
  AIMessage,
21
+ AIMessageChunk,
21
22
  BaseMessage,
22
23
  HumanMessage,
23
24
  SystemMessage,
24
25
  ToolMessage,
25
26
  )
26
- from langchain_core.outputs import ChatGeneration, ChatResult
27
+ from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult
27
28
  from pydantic import ConfigDict
28
29
 
29
30
  from botrun_flow_lang.utils.botrun_logger import get_default_botrun_logger
@@ -193,23 +194,20 @@ class ChatVertexAIClaude(BaseChatModel):
193
194
 
194
195
  return system, merged
195
196
 
196
- def _generate(
197
+ def _build_payload(
197
198
  self,
198
199
  messages: List[BaseMessage],
199
200
  stop: Optional[List[str]] = None,
200
- run_manager: Optional[CallbackManagerForLLMRun] = None,
201
201
  **kwargs,
202
- ) -> ChatResult:
203
- """Call Vertex AI Claude via rawPredict (non-streaming)."""
202
+ ) -> Tuple[str, Dict[str, Any], int]:
203
+ """Build API payload shared by _generate and _stream.
204
+
205
+ Returns:
206
+ (access_token, payload, tools_count)
207
+ """
204
208
  system, api_messages = self._convert_messages(messages)
205
209
  access_token = self._get_access_token()
206
210
 
207
- url = (
208
- f"https://{self.location}-aiplatform.googleapis.com/v1/"
209
- f"projects/{self.project_id}/locations/{self.location}/"
210
- f"publishers/anthropic/models/{self.model}:rawPredict"
211
- )
212
-
213
211
  payload: Dict[str, Any] = {
214
212
  "anthropic_version": "vertex-2023-10-16",
215
213
  "messages": api_messages,
@@ -221,12 +219,10 @@ class ChatVertexAIClaude(BaseChatModel):
221
219
  if stop:
222
220
  payload["stop_sequences"] = stop
223
221
 
224
- # Tools from bind_tools()
225
222
  tools = kwargs.get("tools", [])
226
223
  if tools:
227
224
  payload["tools"] = tools
228
225
 
229
- # Tool choice
230
226
  tool_choice = kwargs.get("tool_choice")
231
227
  if tool_choice:
232
228
  if isinstance(tool_choice, str):
@@ -244,13 +240,32 @@ class ChatVertexAIClaude(BaseChatModel):
244
240
  elif isinstance(tool_choice, dict):
245
241
  payload["tool_choice"] = tool_choice
246
242
 
243
+ return access_token, payload, len(tools)
244
+
245
+ def _generate(
246
+ self,
247
+ messages: List[BaseMessage],
248
+ stop: Optional[List[str]] = None,
249
+ run_manager: Optional[CallbackManagerForLLMRun] = None,
250
+ **kwargs,
251
+ ) -> ChatResult:
252
+ """Call Vertex AI Claude via rawPredict (non-streaming)."""
253
+ access_token, payload, tools_count = self._build_payload(
254
+ messages, stop, **kwargs
255
+ )
256
+
257
+ url = (
258
+ f"https://{self.location}-aiplatform.googleapis.com/v1/"
259
+ f"projects/{self.project_id}/locations/{self.location}/"
260
+ f"publishers/anthropic/models/{self.model}:rawPredict"
261
+ )
262
+
247
263
  logger.info(
248
264
  f"[ChatVertexAIClaude] rawPredict: model={self.model}, "
249
- f"location={self.location}, messages={len(api_messages)}, "
250
- f"tools={len(tools)}"
265
+ f"location={self.location}, messages={len(payload['messages'])}, "
266
+ f"tools={tools_count}"
251
267
  )
252
268
 
253
- # Make API call via httpx
254
269
  data = _http_post_json(url, payload, access_token)
255
270
 
256
271
  # Parse response
@@ -305,6 +320,192 @@ class ChatVertexAIClaude(BaseChatModel):
305
320
  },
306
321
  )
307
322
 
323
+ def _stream(
324
+ self,
325
+ messages: List[BaseMessage],
326
+ stop: Optional[List[str]] = None,
327
+ run_manager: Optional[CallbackManagerForLLMRun] = None,
328
+ **kwargs,
329
+ ) -> Iterator[ChatGenerationChunk]:
330
+ """Call Vertex AI Claude via streamRawPredict (streaming).
331
+
332
+ Yields ChatGenerationChunk with AIMessageChunk for each SSE event.
333
+ Handles both text and tool_use content blocks.
334
+ """
335
+ access_token, payload, tools_count = self._build_payload(
336
+ messages, stop, **kwargs
337
+ )
338
+ payload["stream"] = True
339
+
340
+ url = (
341
+ f"https://{self.location}-aiplatform.googleapis.com/v1/"
342
+ f"projects/{self.project_id}/locations/{self.location}/"
343
+ f"publishers/anthropic/models/{self.model}:streamRawPredict"
344
+ )
345
+
346
+ logger.info(
347
+ f"[ChatVertexAIClaude] streamRawPredict: model={self.model}, "
348
+ f"location={self.location}, messages={len(payload['messages'])}, "
349
+ f"tools={tools_count}"
350
+ )
351
+
352
+ headers = {
353
+ "Content-Type": "application/json",
354
+ "Authorization": f"Bearer {access_token}",
355
+ }
356
+
357
+ # Track state across SSE events
358
+ usage_metadata: Dict[str, int] = {}
359
+ # Map block index -> tool info for tool_use streaming
360
+ tool_use_blocks: Dict[int, Dict[str, Any]] = {}
361
+
362
+ try:
363
+ with httpx.Client(timeout=300.0) as client:
364
+ with client.stream(
365
+ "POST", url, headers=headers, json=payload
366
+ ) as response:
367
+ if response.status_code != 200:
368
+ error_body = response.read().decode("utf-8", errors="ignore")
369
+ error_msg = f"Vertex AI API error: {response.status_code} - {error_body}"
370
+ logger.error(f"[ChatVertexAIClaude] {error_msg}")
371
+ raise Exception(error_msg)
372
+
373
+ for line in response.iter_lines():
374
+ if isinstance(line, bytes):
375
+ line = line.decode("utf-8")
376
+ line = line.strip()
377
+
378
+ if not line or line.startswith("event:"):
379
+ continue
380
+
381
+ if line.startswith("data:"):
382
+ data_str = line[5:].strip()
383
+ else:
384
+ data_str = line
385
+
386
+ try:
387
+ data = json.loads(data_str)
388
+ except json.JSONDecodeError:
389
+ continue
390
+
391
+ if not data:
392
+ continue
393
+
394
+ event_type = data.get("type", "")
395
+
396
+ if event_type == "message_start":
397
+ msg = data.get("message", {})
398
+ if "usage" in msg:
399
+ usage_metadata["input_tokens"] = msg["usage"].get(
400
+ "input_tokens", 0
401
+ )
402
+
403
+ elif event_type == "content_block_start":
404
+ block = data.get("content_block", {})
405
+ index = data.get("index", 0)
406
+ if block.get("type") == "tool_use":
407
+ # Start of a tool_use block
408
+ tool_use_blocks[index] = {
409
+ "id": block.get("id", ""),
410
+ "name": block.get("name", ""),
411
+ "args_json": "",
412
+ }
413
+ chunk = AIMessageChunk(
414
+ content="",
415
+ tool_call_chunks=[
416
+ {
417
+ "name": block.get("name", ""),
418
+ "args": "",
419
+ "id": block.get("id", ""),
420
+ "index": index,
421
+ }
422
+ ],
423
+ )
424
+ yield ChatGenerationChunk(message=chunk)
425
+ if run_manager:
426
+ run_manager.on_llm_new_token(
427
+ "", chunk=chunk
428
+ )
429
+
430
+ elif event_type == "content_block_delta":
431
+ delta = data.get("delta", {})
432
+ index = data.get("index", 0)
433
+ delta_type = delta.get("type", "")
434
+
435
+ if delta_type == "text_delta":
436
+ text = delta.get("text", "")
437
+ if text:
438
+ chunk = AIMessageChunk(content=text)
439
+ yield ChatGenerationChunk(message=chunk)
440
+ if run_manager:
441
+ run_manager.on_llm_new_token(
442
+ text, chunk=chunk
443
+ )
444
+
445
+ elif delta_type == "input_json_delta":
446
+ partial_json = delta.get("partial_json", "")
447
+ if index in tool_use_blocks:
448
+ tool_use_blocks[index][
449
+ "args_json"
450
+ ] += partial_json
451
+ chunk = AIMessageChunk(
452
+ content="",
453
+ tool_call_chunks=[
454
+ {
455
+ "name": None,
456
+ "args": partial_json,
457
+ "id": None,
458
+ "index": index,
459
+ }
460
+ ],
461
+ )
462
+ yield ChatGenerationChunk(message=chunk)
463
+ if run_manager:
464
+ run_manager.on_llm_new_token(
465
+ "", chunk=chunk
466
+ )
467
+
468
+ elif event_type == "message_delta":
469
+ delta = data.get("delta", {})
470
+ if "usage" in data:
471
+ usage_metadata["output_tokens"] = data[
472
+ "usage"
473
+ ].get("output_tokens", 0)
474
+
475
+ elif event_type == "message_stop":
476
+ # Yield final chunk with usage metadata
477
+ input_tokens = usage_metadata.get("input_tokens", 0)
478
+ output_tokens = usage_metadata.get(
479
+ "output_tokens", 0
480
+ )
481
+ chunk = AIMessageChunk(
482
+ content="",
483
+ usage_metadata={
484
+ "input_tokens": input_tokens,
485
+ "output_tokens": output_tokens,
486
+ "total_tokens": input_tokens
487
+ + output_tokens,
488
+ },
489
+ response_metadata={
490
+ "model": self.model,
491
+ },
492
+ )
493
+ yield ChatGenerationChunk(message=chunk)
494
+
495
+ logger.info(
496
+ f"[ChatVertexAIClaude] Stream complete: "
497
+ f"tokens=({input_tokens}+{output_tokens}"
498
+ f"={input_tokens + output_tokens})"
499
+ )
500
+
501
+ except httpx.HTTPStatusError as e:
502
+ error_body = e.response.text if e.response else ""
503
+ error_msg = (
504
+ f"Vertex AI API error: {e.response.status_code} - {error_body}"
505
+ )
506
+ logger.error(f"[ChatVertexAIClaude] {error_msg}")
507
+ raise Exception(error_msg) from e
508
+
308
509
 
309
510
  def _http_post_json(
310
511
  url: str, payload: Dict[str, Any], access_token: str
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: botrun-flow-lang
3
- Version: 6.2.61
3
+ Version: 6.2.62
4
4
  Summary: A flow language for botrun
5
5
  Author-email: sebastian-hsu <sebastian.hsu@gmail.com>
6
6
  License: MIT
@@ -39,7 +39,7 @@ botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_graph.py,
39
39
  botrun_flow_lang/langgraph_agents/agents/tools/__init__.py,sha256=-z1uuC3IET02q8kPhPlr-L9eTGJqgHjEJlC__cG16H0,105
40
40
  botrun_flow_lang/langgraph_agents/agents/tools/gemini_code_execution.py,sha256=EEp8xhVU-Kj1Nk5qV8ObqdVZ8gT6GITrE4VyjIc2InA,14238
41
41
  botrun_flow_lang/langgraph_agents/agents/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
- botrun_flow_lang/langgraph_agents/agents/util/custom_vertex_claude.py,sha256=RDj1-PnoR36xdOUu7r6yMQrxAe-CXyGcSZEKiOlPxJY,14951
42
+ botrun_flow_lang/langgraph_agents/agents/util/custom_vertex_claude.py,sha256=4ZCNILUCJoZMMy_Dh1Slf-KvYuMzhZNamnfDZXhjSQE,24124
43
43
  botrun_flow_lang/langgraph_agents/agents/util/gemini_grounding.py,sha256=JTfH9WJNDlpvMvfzXyZy3bHeCN58MTnEOiamQGMsqh0,2884
44
44
  botrun_flow_lang/langgraph_agents/agents/util/html_util.py,sha256=g5yJO0qTqRq_kb-xhSnWX3WAbHDIjNQYl7ErRBPQwHs,13230
45
45
  botrun_flow_lang/langgraph_agents/agents/util/img_util.py,sha256=6OERtpGGimlev4Pb_O1UbMNaT_DMBHSmAgo9gB-R8xk,12385
@@ -100,6 +100,6 @@ botrun_flow_lang/utils/yaml_utils.py,sha256=dPlabIol-Clhnwc7N5nuffCaLSq8dyvmvjRw
100
100
  botrun_flow_lang/utils/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
101
101
  botrun_flow_lang/utils/clients/rate_limit_client.py,sha256=96NNCHB9I5C5bpVFF6sfPhmh4oAx3UdOLb-Z4PAXLdg,8558
102
102
  botrun_flow_lang/utils/clients/token_verify_client.py,sha256=-AnYApJ9CvxVn-RhCCZZ2LCrf065fgskhwLKAm-aiN0,5893
103
- botrun_flow_lang-6.2.61.dist-info/METADATA,sha256=7mfsw0_BTSzWq5fSeKqCg0RmuOEyMfdKwItnzlRA0p8,6164
104
- botrun_flow_lang-6.2.61.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
105
- botrun_flow_lang-6.2.61.dist-info/RECORD,,
103
+ botrun_flow_lang-6.2.62.dist-info/METADATA,sha256=ZI_XX1JwrqTbJ_SzdzSm1y1Iw882DxfAv2ESCLhJxNw,6164
104
+ botrun_flow_lang-6.2.62.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
105
+ botrun_flow_lang-6.2.62.dist-info/RECORD,,