abstractcore 2.5.3__py3-none-any.whl → 2.6.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.
- abstractcore/__init__.py +7 -1
- abstractcore/architectures/detection.py +2 -2
- abstractcore/core/retry.py +2 -2
- abstractcore/core/session.py +132 -1
- abstractcore/download.py +253 -0
- abstractcore/embeddings/manager.py +2 -2
- abstractcore/events/__init__.py +112 -1
- abstractcore/exceptions/__init__.py +49 -2
- abstractcore/media/processors/office_processor.py +2 -2
- abstractcore/media/utils/image_scaler.py +2 -2
- abstractcore/media/vision_fallback.py +2 -2
- abstractcore/providers/anthropic_provider.py +200 -6
- abstractcore/providers/base.py +100 -5
- abstractcore/providers/lmstudio_provider.py +246 -2
- abstractcore/providers/ollama_provider.py +244 -2
- abstractcore/providers/openai_provider.py +258 -6
- abstractcore/providers/streaming.py +2 -2
- abstractcore/tools/common_tools.py +2 -2
- abstractcore/tools/handler.py +2 -2
- abstractcore/tools/parser.py +2 -2
- abstractcore/tools/registry.py +2 -2
- abstractcore/tools/syntax_rewriter.py +2 -2
- abstractcore/tools/tag_rewriter.py +3 -3
- abstractcore/utils/self_fixes.py +2 -2
- abstractcore/utils/version.py +1 -1
- {abstractcore-2.5.3.dist-info → abstractcore-2.6.0.dist-info}/METADATA +102 -4
- {abstractcore-2.5.3.dist-info → abstractcore-2.6.0.dist-info}/RECORD +31 -30
- {abstractcore-2.5.3.dist-info → abstractcore-2.6.0.dist-info}/WHEEL +0 -0
- {abstractcore-2.5.3.dist-info → abstractcore-2.6.0.dist-info}/entry_points.txt +0 -0
- {abstractcore-2.5.3.dist-info → abstractcore-2.6.0.dist-info}/licenses/LICENSE +0 -0
- {abstractcore-2.5.3.dist-info → abstractcore-2.6.0.dist-info}/top_level.txt +0 -0
|
@@ -6,13 +6,13 @@ This module provides comprehensive processing capabilities for Microsoft Office
|
|
|
6
6
|
document processing in 2025.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
import logging
|
|
10
9
|
from pathlib import Path
|
|
11
10
|
from typing import Optional, Dict, Any, List, Union, Tuple
|
|
12
11
|
import json
|
|
13
12
|
|
|
14
13
|
from ..base import BaseMediaHandler, MediaProcessingError
|
|
15
14
|
from ..types import MediaContent, MediaType, ContentFormat, MediaProcessingResult
|
|
15
|
+
from ...utils.structured_logging import get_logger
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class OfficeProcessor(BaseMediaHandler):
|
|
@@ -36,7 +36,7 @@ class OfficeProcessor(BaseMediaHandler):
|
|
|
36
36
|
**kwargs: Additional configuration options
|
|
37
37
|
"""
|
|
38
38
|
super().__init__(**kwargs)
|
|
39
|
-
self.logger =
|
|
39
|
+
self.logger = get_logger(__name__)
|
|
40
40
|
|
|
41
41
|
# Configuration options
|
|
42
42
|
self.extract_tables = kwargs.get('extract_tables', True)
|
|
@@ -8,7 +8,6 @@ and capabilities for vision models.
|
|
|
8
8
|
from typing import Tuple, Optional, Union, Dict, Any
|
|
9
9
|
from enum import Enum
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
import logging
|
|
12
11
|
|
|
13
12
|
try:
|
|
14
13
|
from PIL import Image, ImageOps
|
|
@@ -17,6 +16,7 @@ except ImportError:
|
|
|
17
16
|
PIL_AVAILABLE = False
|
|
18
17
|
|
|
19
18
|
from ..base import MediaProcessingError
|
|
19
|
+
from ...utils.structured_logging import get_logger
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class ScalingMode(Enum):
|
|
@@ -36,7 +36,7 @@ class ModelOptimizedScaler:
|
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
38
|
def __init__(self):
|
|
39
|
-
self.logger =
|
|
39
|
+
self.logger = get_logger(__name__)
|
|
40
40
|
|
|
41
41
|
if not PIL_AVAILABLE:
|
|
42
42
|
raise MediaProcessingError("PIL (Pillow) is required for image scaling")
|
|
@@ -5,11 +5,11 @@ Implements two-stage pipeline: vision model → description → text-only model
|
|
|
5
5
|
Uses unified AbstractCore configuration system.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
import logging
|
|
9
8
|
from pathlib import Path
|
|
10
9
|
from typing import Optional, Dict, Any
|
|
10
|
+
from ..utils.structured_logging import get_logger
|
|
11
11
|
|
|
12
|
-
logger =
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class VisionNotConfiguredError(Exception):
|
|
@@ -5,7 +5,7 @@ Anthropic provider implementation.
|
|
|
5
5
|
import os
|
|
6
6
|
import json
|
|
7
7
|
import time
|
|
8
|
-
from typing import List, Dict, Any, Optional, Union, Iterator, Type
|
|
8
|
+
from typing import List, Dict, Any, Optional, Union, Iterator, AsyncIterator, Type
|
|
9
9
|
|
|
10
10
|
try:
|
|
11
11
|
from pydantic import BaseModel
|
|
@@ -16,7 +16,7 @@ except ImportError:
|
|
|
16
16
|
from .base import BaseProvider
|
|
17
17
|
from ..core.types import GenerateResponse
|
|
18
18
|
from ..media import MediaHandler
|
|
19
|
-
from ..exceptions import AuthenticationError, ProviderAPIError, ModelNotFoundError, format_model_error
|
|
19
|
+
from ..exceptions import AuthenticationError, ProviderAPIError, ModelNotFoundError, format_model_error, format_auth_error
|
|
20
20
|
from ..tools import UniversalToolHandler, execute_tools
|
|
21
21
|
from ..events import EventType
|
|
22
22
|
|
|
@@ -30,7 +30,8 @@ except ImportError:
|
|
|
30
30
|
class AnthropicProvider(BaseProvider):
|
|
31
31
|
"""Anthropic Claude API provider with full integration"""
|
|
32
32
|
|
|
33
|
-
def __init__(self, model: str = "claude-3-haiku-20240307", api_key: Optional[str] = None,
|
|
33
|
+
def __init__(self, model: str = "claude-3-haiku-20240307", api_key: Optional[str] = None,
|
|
34
|
+
base_url: Optional[str] = None, **kwargs):
|
|
34
35
|
super().__init__(model, **kwargs)
|
|
35
36
|
self.provider = "anthropic"
|
|
36
37
|
|
|
@@ -42,8 +43,15 @@ class AnthropicProvider(BaseProvider):
|
|
|
42
43
|
if not self.api_key:
|
|
43
44
|
raise ValueError("Anthropic API key required. Set ANTHROPIC_API_KEY environment variable.")
|
|
44
45
|
|
|
45
|
-
#
|
|
46
|
-
self.
|
|
46
|
+
# Get base URL from param or environment
|
|
47
|
+
self.base_url = base_url or os.getenv("ANTHROPIC_BASE_URL")
|
|
48
|
+
|
|
49
|
+
# Initialize client with timeout and optional base_url
|
|
50
|
+
client_kwargs = {"api_key": self.api_key, "timeout": self._timeout}
|
|
51
|
+
if self.base_url:
|
|
52
|
+
client_kwargs["base_url"] = self.base_url
|
|
53
|
+
self.client = anthropic.Anthropic(**client_kwargs)
|
|
54
|
+
self._async_client = None # Lazy-loaded async client
|
|
47
55
|
|
|
48
56
|
# Initialize tool handler
|
|
49
57
|
self.tool_handler = UniversalToolHandler(model)
|
|
@@ -56,6 +64,16 @@ class AnthropicProvider(BaseProvider):
|
|
|
56
64
|
"""Public generate method that includes telemetry"""
|
|
57
65
|
return self.generate_with_telemetry(*args, **kwargs)
|
|
58
66
|
|
|
67
|
+
@property
|
|
68
|
+
def async_client(self):
|
|
69
|
+
"""Lazy-load AsyncAnthropic client for native async operations."""
|
|
70
|
+
if self._async_client is None:
|
|
71
|
+
client_kwargs = {"api_key": self.api_key, "timeout": self._timeout}
|
|
72
|
+
if self.base_url:
|
|
73
|
+
client_kwargs["base_url"] = self.base_url
|
|
74
|
+
self._async_client = anthropic.AsyncAnthropic(**client_kwargs)
|
|
75
|
+
return self._async_client
|
|
76
|
+
|
|
59
77
|
def _generate_internal(self,
|
|
60
78
|
prompt: str,
|
|
61
79
|
messages: Optional[List[Dict[str, str]]] = None,
|
|
@@ -207,7 +225,7 @@ class AnthropicProvider(BaseProvider):
|
|
|
207
225
|
error_str = str(e).lower()
|
|
208
226
|
|
|
209
227
|
if 'api_key' in error_str or 'authentication' in error_str:
|
|
210
|
-
raise AuthenticationError(
|
|
228
|
+
raise AuthenticationError(format_auth_error("anthropic", str(e)))
|
|
211
229
|
elif ('not_found_error' in error_str and 'model:' in error_str) or '404' in error_str:
|
|
212
230
|
# Model not found - show available models
|
|
213
231
|
available_models = self.list_available_models(api_key=self.api_key)
|
|
@@ -216,6 +234,182 @@ class AnthropicProvider(BaseProvider):
|
|
|
216
234
|
else:
|
|
217
235
|
raise ProviderAPIError(f"Anthropic API error: {str(e)}")
|
|
218
236
|
|
|
237
|
+
async def _agenerate_internal(self,
|
|
238
|
+
prompt: str,
|
|
239
|
+
messages: Optional[List[Dict[str, str]]] = None,
|
|
240
|
+
system_prompt: Optional[str] = None,
|
|
241
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
242
|
+
media: Optional[List['MediaContent']] = None,
|
|
243
|
+
stream: bool = False,
|
|
244
|
+
response_model: Optional[Type[BaseModel]] = None,
|
|
245
|
+
**kwargs) -> Union[GenerateResponse, AsyncIterator[GenerateResponse]]:
|
|
246
|
+
"""Native async implementation using AsyncAnthropic - 3-10x faster for batch operations."""
|
|
247
|
+
|
|
248
|
+
# Build messages array (same logic as sync)
|
|
249
|
+
api_messages = []
|
|
250
|
+
|
|
251
|
+
# Add conversation history
|
|
252
|
+
if messages:
|
|
253
|
+
for msg in messages:
|
|
254
|
+
# Skip system messages as they're handled separately
|
|
255
|
+
if msg.get("role") != "system":
|
|
256
|
+
# Convert assistant role if needed
|
|
257
|
+
role = msg["role"]
|
|
258
|
+
if role == "assistant":
|
|
259
|
+
api_messages.append({
|
|
260
|
+
"role": "assistant",
|
|
261
|
+
"content": msg["content"]
|
|
262
|
+
})
|
|
263
|
+
else:
|
|
264
|
+
api_messages.append({
|
|
265
|
+
"role": "user",
|
|
266
|
+
"content": msg["content"]
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
# Add current prompt as user message
|
|
270
|
+
if prompt and prompt not in [msg.get("content") for msg in (messages or [])]:
|
|
271
|
+
# Handle multimodal message with media content
|
|
272
|
+
if media:
|
|
273
|
+
try:
|
|
274
|
+
from ..media.handlers import AnthropicMediaHandler
|
|
275
|
+
media_handler = AnthropicMediaHandler(self.model_capabilities)
|
|
276
|
+
|
|
277
|
+
# Create multimodal message combining text and media
|
|
278
|
+
multimodal_message = media_handler.create_multimodal_message(prompt, media)
|
|
279
|
+
api_messages.append(multimodal_message)
|
|
280
|
+
except ImportError:
|
|
281
|
+
self.logger.warning("Media processing not available. Install with: pip install abstractcore[media]")
|
|
282
|
+
api_messages.append({"role": "user", "content": prompt})
|
|
283
|
+
except Exception as e:
|
|
284
|
+
self.logger.warning(f"Failed to process media content: {e}")
|
|
285
|
+
api_messages.append({"role": "user", "content": prompt})
|
|
286
|
+
else:
|
|
287
|
+
api_messages.append({"role": "user", "content": prompt})
|
|
288
|
+
|
|
289
|
+
# Prepare API call parameters (same logic as sync)
|
|
290
|
+
generation_kwargs = self._prepare_generation_kwargs(**kwargs)
|
|
291
|
+
max_output_tokens = self._get_provider_max_tokens_param(generation_kwargs)
|
|
292
|
+
|
|
293
|
+
call_params = {
|
|
294
|
+
"model": self.model,
|
|
295
|
+
"messages": api_messages,
|
|
296
|
+
"max_tokens": max_output_tokens,
|
|
297
|
+
"temperature": kwargs.get("temperature", self.temperature),
|
|
298
|
+
"stream": stream
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
# Add system prompt if provided (Anthropic-specific: separate parameter)
|
|
302
|
+
if system_prompt:
|
|
303
|
+
call_params["system"] = system_prompt
|
|
304
|
+
|
|
305
|
+
# Add top_p if specified
|
|
306
|
+
if kwargs.get("top_p") or self.top_p < 1.0:
|
|
307
|
+
call_params["top_p"] = kwargs.get("top_p", self.top_p)
|
|
308
|
+
|
|
309
|
+
# Add top_k if specified
|
|
310
|
+
if kwargs.get("top_k") or self.top_k:
|
|
311
|
+
call_params["top_k"] = kwargs.get("top_k", self.top_k)
|
|
312
|
+
|
|
313
|
+
# Handle seed parameter (Anthropic doesn't support seed natively)
|
|
314
|
+
seed_value = kwargs.get("seed", self.seed)
|
|
315
|
+
if seed_value is not None:
|
|
316
|
+
import warnings
|
|
317
|
+
warnings.warn(
|
|
318
|
+
f"Seed parameter ({seed_value}) is not supported by Anthropic Claude API. "
|
|
319
|
+
f"For deterministic outputs, use temperature=0.0 which may provide more consistent results, "
|
|
320
|
+
f"though true determinism is not guaranteed.",
|
|
321
|
+
UserWarning,
|
|
322
|
+
stacklevel=3
|
|
323
|
+
)
|
|
324
|
+
self.logger.warning(f"Seed {seed_value} requested but not supported by Anthropic API")
|
|
325
|
+
|
|
326
|
+
# Handle structured output using the "tool trick"
|
|
327
|
+
structured_tool_name = None
|
|
328
|
+
if response_model and PYDANTIC_AVAILABLE:
|
|
329
|
+
structured_tool = self._create_structured_output_tool(response_model)
|
|
330
|
+
|
|
331
|
+
if tools:
|
|
332
|
+
tools = list(tools) + [structured_tool]
|
|
333
|
+
else:
|
|
334
|
+
tools = [structured_tool]
|
|
335
|
+
|
|
336
|
+
structured_tool_name = structured_tool["name"]
|
|
337
|
+
|
|
338
|
+
if api_messages and api_messages[-1]["role"] == "user":
|
|
339
|
+
api_messages[-1]["content"] += f"\n\nPlease use the {structured_tool_name} tool to provide your response."
|
|
340
|
+
|
|
341
|
+
# Add tools if provided
|
|
342
|
+
if tools:
|
|
343
|
+
if self.tool_handler.supports_native:
|
|
344
|
+
call_params["tools"] = self._format_tools_for_anthropic(tools)
|
|
345
|
+
|
|
346
|
+
if structured_tool_name:
|
|
347
|
+
call_params["tool_choice"] = {"type": "tool", "name": structured_tool_name}
|
|
348
|
+
elif kwargs.get("tool_choice"):
|
|
349
|
+
call_params["tool_choice"] = {"type": kwargs.get("tool_choice", "auto")}
|
|
350
|
+
else:
|
|
351
|
+
tool_prompt = self.tool_handler.format_tools_prompt(tools)
|
|
352
|
+
if call_params.get("system"):
|
|
353
|
+
call_params["system"] += f"\n\n{tool_prompt}"
|
|
354
|
+
else:
|
|
355
|
+
call_params["system"] = tool_prompt
|
|
356
|
+
|
|
357
|
+
# Make async API call
|
|
358
|
+
try:
|
|
359
|
+
if stream:
|
|
360
|
+
return self._async_stream_response(call_params, tools)
|
|
361
|
+
else:
|
|
362
|
+
start_time = time.time()
|
|
363
|
+
response = await self.async_client.messages.create(**call_params)
|
|
364
|
+
gen_time = round((time.time() - start_time) * 1000, 1)
|
|
365
|
+
|
|
366
|
+
formatted = self._format_response(response)
|
|
367
|
+
formatted.gen_time = gen_time
|
|
368
|
+
|
|
369
|
+
if tools and (formatted.has_tool_calls() or
|
|
370
|
+
(self.tool_handler.supports_prompted and formatted.content)):
|
|
371
|
+
formatted = self._handle_tool_execution(formatted, tools)
|
|
372
|
+
|
|
373
|
+
return formatted
|
|
374
|
+
except Exception as e:
|
|
375
|
+
error_str = str(e).lower()
|
|
376
|
+
|
|
377
|
+
if 'api_key' in error_str or 'authentication' in error_str:
|
|
378
|
+
raise AuthenticationError(format_auth_error("anthropic", str(e)))
|
|
379
|
+
elif ('not_found_error' in error_str and 'model:' in error_str) or '404' in error_str:
|
|
380
|
+
available_models = self.list_available_models(api_key=self.api_key)
|
|
381
|
+
error_message = format_model_error("Anthropic", self.model, available_models)
|
|
382
|
+
raise ModelNotFoundError(error_message)
|
|
383
|
+
else:
|
|
384
|
+
raise ProviderAPIError(f"Anthropic API error: {str(e)}")
|
|
385
|
+
|
|
386
|
+
async def _async_stream_response(self, call_params: Dict[str, Any], tools: Optional[List[Dict[str, Any]]] = None) -> AsyncIterator[GenerateResponse]:
|
|
387
|
+
"""Native async streaming with Anthropic's context manager pattern."""
|
|
388
|
+
stream_params = {k: v for k, v in call_params.items() if k != 'stream'}
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
async with self.async_client.messages.stream(**stream_params) as stream:
|
|
392
|
+
async for chunk in stream:
|
|
393
|
+
yield GenerateResponse(
|
|
394
|
+
content=getattr(chunk, 'content', ''),
|
|
395
|
+
model=self.model,
|
|
396
|
+
finish_reason=getattr(chunk, 'finish_reason', None),
|
|
397
|
+
raw_response=chunk
|
|
398
|
+
)
|
|
399
|
+
except Exception as e:
|
|
400
|
+
raise ProviderAPIError(f"Anthropic streaming error: {str(e)}")
|
|
401
|
+
|
|
402
|
+
def unload(self) -> None:
|
|
403
|
+
"""Close async client if it was created."""
|
|
404
|
+
if self._async_client is not None:
|
|
405
|
+
import asyncio
|
|
406
|
+
try:
|
|
407
|
+
loop = asyncio.get_running_loop()
|
|
408
|
+
loop.create_task(self._async_client.close())
|
|
409
|
+
except RuntimeError:
|
|
410
|
+
import asyncio
|
|
411
|
+
asyncio.run(self._async_client.close())
|
|
412
|
+
|
|
219
413
|
def _format_tools_for_anthropic(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
220
414
|
"""Format tools for Anthropic API format"""
|
|
221
415
|
formatted_tools = []
|
abstractcore/providers/base.py
CHANGED
|
@@ -4,8 +4,9 @@ Base provider with integrated telemetry, events, and exception handling.
|
|
|
4
4
|
|
|
5
5
|
import time
|
|
6
6
|
import uuid
|
|
7
|
+
import asyncio
|
|
7
8
|
from collections import deque
|
|
8
|
-
from typing import List, Dict, Any, Optional, Union, Iterator, Type
|
|
9
|
+
from typing import List, Dict, Any, Optional, Union, Iterator, AsyncIterator, Type
|
|
9
10
|
from abc import ABC, abstractmethod
|
|
10
11
|
|
|
11
12
|
try:
|
|
@@ -1440,9 +1441,9 @@ Please provide a structured response."""
|
|
|
1440
1441
|
**kwargs) -> Union[GenerateResponse, Iterator[GenerateResponse], BaseModel]:
|
|
1441
1442
|
"""
|
|
1442
1443
|
Generate response from the LLM.
|
|
1443
|
-
|
|
1444
|
+
|
|
1444
1445
|
This method implements the AbstractCoreInterface and delegates to generate_with_telemetry.
|
|
1445
|
-
|
|
1446
|
+
|
|
1446
1447
|
Args:
|
|
1447
1448
|
prompt: The input prompt
|
|
1448
1449
|
messages: Optional conversation history
|
|
@@ -1450,7 +1451,7 @@ Please provide a structured response."""
|
|
|
1450
1451
|
tools: Optional list of available tools
|
|
1451
1452
|
stream: Whether to stream the response
|
|
1452
1453
|
**kwargs: Additional provider-specific parameters (including response_model)
|
|
1453
|
-
|
|
1454
|
+
|
|
1454
1455
|
Returns:
|
|
1455
1456
|
GenerateResponse, iterator of GenerateResponse for streaming, or BaseModel for structured output
|
|
1456
1457
|
"""
|
|
@@ -1461,4 +1462,98 @@ Please provide a structured response."""
|
|
|
1461
1462
|
tools=tools,
|
|
1462
1463
|
stream=stream,
|
|
1463
1464
|
**kwargs
|
|
1464
|
-
)
|
|
1465
|
+
)
|
|
1466
|
+
|
|
1467
|
+
async def agenerate(self,
|
|
1468
|
+
prompt: str = "",
|
|
1469
|
+
messages: Optional[List[Dict]] = None,
|
|
1470
|
+
system_prompt: Optional[str] = None,
|
|
1471
|
+
tools: Optional[List] = None,
|
|
1472
|
+
media: Optional[List] = None,
|
|
1473
|
+
stream: bool = False,
|
|
1474
|
+
**kwargs) -> Union[GenerateResponse, AsyncIterator[GenerateResponse], BaseModel]:
|
|
1475
|
+
"""
|
|
1476
|
+
Async generation - works with all providers.
|
|
1477
|
+
|
|
1478
|
+
Calls _agenerate_internal() which can be overridden for native async.
|
|
1479
|
+
Default implementation uses asyncio.to_thread() fallback.
|
|
1480
|
+
|
|
1481
|
+
Args:
|
|
1482
|
+
prompt: Text prompt
|
|
1483
|
+
messages: Conversation history
|
|
1484
|
+
system_prompt: System instructions
|
|
1485
|
+
tools: Available tools
|
|
1486
|
+
media: Media attachments
|
|
1487
|
+
stream: Enable streaming
|
|
1488
|
+
**kwargs: Additional generation parameters (including response_model)
|
|
1489
|
+
|
|
1490
|
+
Returns:
|
|
1491
|
+
GenerateResponse, AsyncIterator[GenerateResponse] for streaming, or BaseModel for structured output
|
|
1492
|
+
"""
|
|
1493
|
+
return await self._agenerate_internal(
|
|
1494
|
+
prompt, messages, system_prompt, tools, media, stream, **kwargs
|
|
1495
|
+
)
|
|
1496
|
+
|
|
1497
|
+
async def _agenerate_internal(self,
|
|
1498
|
+
prompt: str,
|
|
1499
|
+
messages: Optional[List[Dict]],
|
|
1500
|
+
system_prompt: Optional[str],
|
|
1501
|
+
tools: Optional[List],
|
|
1502
|
+
media: Optional[List],
|
|
1503
|
+
stream: bool,
|
|
1504
|
+
**kwargs) -> Union[GenerateResponse, AsyncIterator[GenerateResponse], BaseModel]:
|
|
1505
|
+
"""
|
|
1506
|
+
Internal async generation method.
|
|
1507
|
+
|
|
1508
|
+
Default implementation: Uses asyncio.to_thread() to run sync generate().
|
|
1509
|
+
Providers override this for native async (3-10x faster for batch operations).
|
|
1510
|
+
|
|
1511
|
+
Args:
|
|
1512
|
+
prompt: Text prompt
|
|
1513
|
+
messages: Conversation history
|
|
1514
|
+
system_prompt: System instructions
|
|
1515
|
+
tools: Available tools
|
|
1516
|
+
media: Media attachments
|
|
1517
|
+
stream: Enable streaming
|
|
1518
|
+
**kwargs: Additional generation parameters
|
|
1519
|
+
|
|
1520
|
+
Returns:
|
|
1521
|
+
GenerateResponse, AsyncIterator[GenerateResponse] for streaming, or BaseModel for structured output
|
|
1522
|
+
"""
|
|
1523
|
+
if stream:
|
|
1524
|
+
# Return async iterator for streaming
|
|
1525
|
+
return self._async_stream_generate(
|
|
1526
|
+
prompt, messages, system_prompt, tools, media, **kwargs
|
|
1527
|
+
)
|
|
1528
|
+
else:
|
|
1529
|
+
# Run sync generate in thread pool (fallback)
|
|
1530
|
+
return await asyncio.to_thread(
|
|
1531
|
+
self.generate,
|
|
1532
|
+
prompt, messages, system_prompt, tools, stream, **kwargs
|
|
1533
|
+
)
|
|
1534
|
+
|
|
1535
|
+
async def _async_stream_generate(self,
|
|
1536
|
+
prompt: str,
|
|
1537
|
+
messages: Optional[List[Dict]],
|
|
1538
|
+
system_prompt: Optional[str],
|
|
1539
|
+
tools: Optional[List],
|
|
1540
|
+
media: Optional[List],
|
|
1541
|
+
**kwargs) -> AsyncIterator[GenerateResponse]:
|
|
1542
|
+
"""
|
|
1543
|
+
Async streaming generator.
|
|
1544
|
+
|
|
1545
|
+
Wraps sync streaming in async iterator, yielding control to event loop.
|
|
1546
|
+
"""
|
|
1547
|
+
# Get sync generator in thread pool
|
|
1548
|
+
def get_sync_stream():
|
|
1549
|
+
return self.generate(
|
|
1550
|
+
prompt, messages, system_prompt, tools,
|
|
1551
|
+
stream=True, **kwargs
|
|
1552
|
+
)
|
|
1553
|
+
|
|
1554
|
+
sync_gen = await asyncio.to_thread(get_sync_stream)
|
|
1555
|
+
|
|
1556
|
+
# Yield chunks asynchronously
|
|
1557
|
+
for chunk in sync_gen:
|
|
1558
|
+
yield chunk
|
|
1559
|
+
await asyncio.sleep(0) # Yield control to event loop
|