apple-foundation-models 0.2.2__cp312-cp312-macosx_26_0_universal2.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.
- apple_foundation_models-0.2.2.dist-info/METADATA +620 -0
- apple_foundation_models-0.2.2.dist-info/RECORD +20 -0
- apple_foundation_models-0.2.2.dist-info/WHEEL +5 -0
- apple_foundation_models-0.2.2.dist-info/licenses/LICENSE +21 -0
- apple_foundation_models-0.2.2.dist-info/top_level.txt +1 -0
- applefoundationmodels/__init__.py +124 -0
- applefoundationmodels/_foundationmodels.cpython-312-darwin.so +0 -0
- applefoundationmodels/_foundationmodels.pyi +43 -0
- applefoundationmodels/async_session.py +296 -0
- applefoundationmodels/base.py +65 -0
- applefoundationmodels/base_session.py +659 -0
- applefoundationmodels/constants.py +30 -0
- applefoundationmodels/error_codes.json +163 -0
- applefoundationmodels/exceptions.py +122 -0
- applefoundationmodels/libfoundation_models.dylib +0 -0
- applefoundationmodels/py.typed +2 -0
- applefoundationmodels/pydantic_compat.py +144 -0
- applefoundationmodels/session.py +265 -0
- applefoundationmodels/tools.py +285 -0
- applefoundationmodels/types.py +284 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"code": -1,
|
|
4
|
+
"name": "InitializationError",
|
|
5
|
+
"swift_case": null,
|
|
6
|
+
"parent": "FoundationModelsError",
|
|
7
|
+
"description": "Library initialization failed."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"code": -2,
|
|
11
|
+
"name": "NotAvailableError",
|
|
12
|
+
"swift_case": "errorNotAvailable",
|
|
13
|
+
"parent": "FoundationModelsError",
|
|
14
|
+
"description": "Apple Intelligence not available on this device."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"code": -3,
|
|
18
|
+
"name": "InvalidParametersError",
|
|
19
|
+
"swift_case": "errorInvalidParams",
|
|
20
|
+
"parent": "FoundationModelsError",
|
|
21
|
+
"description": "Invalid parameters provided to function."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"code": -4,
|
|
25
|
+
"name": "MemoryError",
|
|
26
|
+
"swift_case": "errorMemory",
|
|
27
|
+
"parent": "FoundationModelsError",
|
|
28
|
+
"description": "Memory allocation error."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"code": -5,
|
|
32
|
+
"name": "JSONParseError",
|
|
33
|
+
"swift_case": "errorJSONParse",
|
|
34
|
+
"parent": "FoundationModelsError",
|
|
35
|
+
"description": "JSON parsing or validation error."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"code": -6,
|
|
39
|
+
"name": "GenerationError",
|
|
40
|
+
"swift_case": "errorGeneration",
|
|
41
|
+
"parent": "FoundationModelsError",
|
|
42
|
+
"description": "Text generation error."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"code": -7,
|
|
46
|
+
"name": "TimeoutError",
|
|
47
|
+
"swift_case": "errorTimeout",
|
|
48
|
+
"parent": "FoundationModelsError",
|
|
49
|
+
"description": "Operation timeout."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"code": -8,
|
|
53
|
+
"name": "SessionNotFoundError",
|
|
54
|
+
"swift_case": null,
|
|
55
|
+
"parent": "FoundationModelsError",
|
|
56
|
+
"description": "Session ID not found."
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"code": -9,
|
|
60
|
+
"name": "StreamNotFoundError",
|
|
61
|
+
"swift_case": null,
|
|
62
|
+
"parent": "FoundationModelsError",
|
|
63
|
+
"description": "Stream ID not found or already completed."
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"code": -10,
|
|
67
|
+
"name": "GuardrailViolationError",
|
|
68
|
+
"swift_case": "errorGuardrailViolation",
|
|
69
|
+
"parent": "FoundationModelsError",
|
|
70
|
+
"description": "Content blocked by safety filters."
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"code": -11,
|
|
74
|
+
"name": "ToolNotFoundError",
|
|
75
|
+
"swift_case": "errorToolNotFound",
|
|
76
|
+
"parent": "FoundationModelsError",
|
|
77
|
+
"description": "Tool callback not registered for session."
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"code": -12,
|
|
81
|
+
"name": "ToolExecutionError",
|
|
82
|
+
"swift_case": "errorToolExecution",
|
|
83
|
+
"parent": "FoundationModelsError",
|
|
84
|
+
"description": "Tool execution failed or returned invalid result."
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"code": -13,
|
|
88
|
+
"name": "BufferTooSmallError",
|
|
89
|
+
"swift_case": null,
|
|
90
|
+
"parent": "FoundationModelsError",
|
|
91
|
+
"description": "Internal buffer too small to serialize error message."
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"code": -14,
|
|
95
|
+
"name": "ContextWindowExceededError",
|
|
96
|
+
"swift_case": "errorContextWindowExceeded",
|
|
97
|
+
"parent": "GenerationError",
|
|
98
|
+
"description": "Context window size limit exceeded (4096 tokens)."
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"code": -15,
|
|
102
|
+
"name": "DecodingFailureError",
|
|
103
|
+
"swift_case": "errorDecodingFailure",
|
|
104
|
+
"parent": "GenerationError",
|
|
105
|
+
"description": "Failed to deserialize a valid generable type from model output."
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"code": -16,
|
|
109
|
+
"name": "RateLimitedError",
|
|
110
|
+
"swift_case": "errorRateLimited",
|
|
111
|
+
"parent": "GenerationError",
|
|
112
|
+
"description": "Session has been rate limited."
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"code": -17,
|
|
116
|
+
"name": "RefusalError",
|
|
117
|
+
"swift_case": "errorRefusal",
|
|
118
|
+
"parent": "GenerationError",
|
|
119
|
+
"description": "Session refused the request."
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"code": -18,
|
|
123
|
+
"name": "ConcurrentRequestsError",
|
|
124
|
+
"swift_case": "errorConcurrentRequests",
|
|
125
|
+
"parent": "GenerationError",
|
|
126
|
+
"description": "Attempted to make a session respond to a second prompt while responding to the first."
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"code": -19,
|
|
130
|
+
"name": "UnsupportedGuideError",
|
|
131
|
+
"swift_case": "errorUnsupportedGuide",
|
|
132
|
+
"parent": "GenerationError",
|
|
133
|
+
"description": "Generation guide with an unsupported pattern was used."
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"code": -20,
|
|
137
|
+
"name": "UnsupportedLanguageError",
|
|
138
|
+
"swift_case": "errorUnsupportedLanguage",
|
|
139
|
+
"parent": "GenerationError",
|
|
140
|
+
"description": "Model was prompted to respond in a language that it does not support."
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"code": -21,
|
|
144
|
+
"name": "AssetsUnavailableError",
|
|
145
|
+
"swift_case": "errorAssetsUnavailable",
|
|
146
|
+
"parent": "GenerationError",
|
|
147
|
+
"description": "Assets required for the session are unavailable."
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"code": -98,
|
|
151
|
+
"name": "ToolCallError",
|
|
152
|
+
"swift_case": null,
|
|
153
|
+
"parent": "FoundationModelsError",
|
|
154
|
+
"description": "Tool call error (validation, schema, etc.)."
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"code": -99,
|
|
158
|
+
"name": "UnknownError",
|
|
159
|
+
"swift_case": "errorUnknown",
|
|
160
|
+
"parent": "FoundationModelsError",
|
|
161
|
+
"description": "Unknown error occurred."
|
|
162
|
+
}
|
|
163
|
+
]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exception hierarchy for applefoundationmodels Python bindings.
|
|
3
|
+
|
|
4
|
+
Exceptions are generated from a single error_codes.json definition so both
|
|
5
|
+
Python and Swift share the same source of truth for error metadata.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from importlib import resources
|
|
13
|
+
from typing import Any, Dict, List, Optional, Tuple, Type
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"FoundationModelsError",
|
|
17
|
+
"GenerationError",
|
|
18
|
+
"ERROR_CODE_TO_EXCEPTION",
|
|
19
|
+
"raise_for_error_code",
|
|
20
|
+
"get_error_definitions",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FoundationModelsError(Exception):
|
|
25
|
+
"""Base exception for all FoundationModels errors."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, message: str, error_code: Optional[int] = None):
|
|
28
|
+
super().__init__(message)
|
|
29
|
+
self.message = message
|
|
30
|
+
self.error_code = error_code
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GenerationError(FoundationModelsError):
|
|
34
|
+
"""Text generation error."""
|
|
35
|
+
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class ErrorDefinition:
|
|
41
|
+
"""Structured representation of an error entry from the JSON definition."""
|
|
42
|
+
|
|
43
|
+
code: int
|
|
44
|
+
name: str
|
|
45
|
+
parent: str
|
|
46
|
+
description: str
|
|
47
|
+
swift_case: Optional[str]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _load_error_definitions() -> Tuple[ErrorDefinition, ...]:
|
|
51
|
+
data_path = resources.files(__package__).joinpath("error_codes.json")
|
|
52
|
+
with data_path.open("r", encoding="utf-8") as f:
|
|
53
|
+
raw: List[Dict[str, Any]] = json.load(f)
|
|
54
|
+
|
|
55
|
+
definitions: List[ErrorDefinition] = []
|
|
56
|
+
for entry in raw:
|
|
57
|
+
definitions.append(
|
|
58
|
+
ErrorDefinition(
|
|
59
|
+
code=int(entry["code"]),
|
|
60
|
+
name=str(entry["name"]),
|
|
61
|
+
parent=str(entry.get("parent", "FoundationModelsError")),
|
|
62
|
+
description=str(entry["description"]),
|
|
63
|
+
swift_case=entry.get("swift_case") or None,
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
return tuple(definitions)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
_ERROR_DEFINITIONS = _load_error_definitions()
|
|
70
|
+
|
|
71
|
+
_PARENT_CLASS_MAP: Dict[str, Type[FoundationModelsError]] = {
|
|
72
|
+
"FoundationModelsError": FoundationModelsError,
|
|
73
|
+
"GenerationError": GenerationError,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_error_definitions() -> Tuple[ErrorDefinition, ...]:
|
|
78
|
+
"""Return the error definitions used to build the exception hierarchy."""
|
|
79
|
+
|
|
80
|
+
return _ERROR_DEFINITIONS
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
ERROR_CODE_TO_EXCEPTION: Dict[int, Type[FoundationModelsError]] = {}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _register_exception_classes() -> None:
|
|
87
|
+
for definition in _ERROR_DEFINITIONS:
|
|
88
|
+
parent_class = _PARENT_CLASS_MAP.get(definition.parent, FoundationModelsError)
|
|
89
|
+
|
|
90
|
+
if definition.name == "GenerationError":
|
|
91
|
+
ERROR_CODE_TO_EXCEPTION[definition.code] = GenerationError
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
exception_class = type(
|
|
95
|
+
definition.name,
|
|
96
|
+
(parent_class,),
|
|
97
|
+
{"__doc__": definition.description},
|
|
98
|
+
)
|
|
99
|
+
globals()[definition.name] = exception_class
|
|
100
|
+
__all__.append(definition.name)
|
|
101
|
+
ERROR_CODE_TO_EXCEPTION[definition.code] = exception_class
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
_register_exception_classes()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def raise_for_error_code(error_code: int, message: str) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Raise the appropriate exception for a given error code.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
error_code: The error code from the Swift API
|
|
113
|
+
message: Error message to include in the exception
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
FoundationModelsError: The appropriate exception subclass for the error code
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
exception_class = ERROR_CODE_TO_EXCEPTION.get(
|
|
120
|
+
error_code, ERROR_CODE_TO_EXCEPTION.get(-99, FoundationModelsError)
|
|
121
|
+
)
|
|
122
|
+
raise exception_class(message, error_code)
|
|
Binary file
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic compatibility utilities for applefoundationmodels.
|
|
3
|
+
|
|
4
|
+
Provides optional Pydantic integration for structured output generation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, Union, TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
# Try to import Pydantic, but don't fail if it's not installed
|
|
13
|
+
try:
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
|
|
16
|
+
PYDANTIC_AVAILABLE = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
PYDANTIC_AVAILABLE = False
|
|
19
|
+
BaseModel = None # type: ignore
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def require_pydantic() -> None:
|
|
23
|
+
"""
|
|
24
|
+
Raise ImportError if Pydantic is not available.
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
ImportError: If Pydantic is not installed
|
|
28
|
+
"""
|
|
29
|
+
if not PYDANTIC_AVAILABLE:
|
|
30
|
+
raise ImportError(
|
|
31
|
+
"Pydantic is not installed. Install it with: pip install pydantic>=2.0"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def model_to_schema(model: "BaseModel") -> Dict[str, Any]:
|
|
36
|
+
"""
|
|
37
|
+
Convert a Pydantic model to JSON Schema.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
model: Pydantic BaseModel class or instance
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
JSON Schema dictionary
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ImportError: If Pydantic is not installed
|
|
47
|
+
ValueError: If model is not a valid Pydantic model
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
>>> from pydantic import BaseModel
|
|
51
|
+
>>> class Person(BaseModel):
|
|
52
|
+
... name: str
|
|
53
|
+
... age: int
|
|
54
|
+
>>> schema = model_to_schema(Person)
|
|
55
|
+
>>> print(schema)
|
|
56
|
+
{'type': 'object', 'properties': {...}, 'required': [...]}
|
|
57
|
+
"""
|
|
58
|
+
require_pydantic()
|
|
59
|
+
|
|
60
|
+
if not hasattr(model, "model_json_schema"):
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"Expected Pydantic BaseModel, got {type(model).__name__}. "
|
|
63
|
+
"Make sure your model inherits from pydantic.BaseModel"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Get JSON Schema from Pydantic model
|
|
67
|
+
schema = model.model_json_schema()
|
|
68
|
+
|
|
69
|
+
# Clean up schema (remove title, $defs if empty, etc.)
|
|
70
|
+
schema = _clean_schema(schema)
|
|
71
|
+
|
|
72
|
+
return schema
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _clean_schema(schema: Dict[str, Any]) -> Dict[str, Any]:
|
|
76
|
+
"""
|
|
77
|
+
Clean up Pydantic-generated schema for better compatibility.
|
|
78
|
+
|
|
79
|
+
Removes unnecessary fields like 'title' and simplifies nested definitions.
|
|
80
|
+
"""
|
|
81
|
+
cleaned = schema.copy()
|
|
82
|
+
|
|
83
|
+
# Remove title if present
|
|
84
|
+
cleaned.pop("title", None)
|
|
85
|
+
|
|
86
|
+
# Remove $defs if empty or inline them if simple
|
|
87
|
+
if "$defs" in cleaned:
|
|
88
|
+
defs = cleaned["$defs"]
|
|
89
|
+
if not defs:
|
|
90
|
+
del cleaned["$defs"]
|
|
91
|
+
|
|
92
|
+
return cleaned
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def is_pydantic_model(obj: Any) -> bool:
|
|
96
|
+
"""
|
|
97
|
+
Check if an object is a Pydantic model class or instance.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
obj: Object to check
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
True if obj is a Pydantic BaseModel class or instance
|
|
104
|
+
"""
|
|
105
|
+
if not PYDANTIC_AVAILABLE:
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
# Check if it has the Pydantic model_json_schema method
|
|
109
|
+
# This works for both classes and instances
|
|
110
|
+
if hasattr(obj, "model_json_schema"):
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def normalize_schema(schema: Union[Dict[str, Any], "BaseModel"]) -> Dict[str, Any]:
|
|
117
|
+
"""
|
|
118
|
+
Normalize schema input to JSON Schema dict.
|
|
119
|
+
|
|
120
|
+
Accepts either a JSON Schema dictionary or a Pydantic model,
|
|
121
|
+
and returns a JSON Schema dictionary.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
schema: JSON Schema dict or Pydantic BaseModel
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
JSON Schema dictionary
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
TypeError: If schema is neither dict nor Pydantic model
|
|
131
|
+
ImportError: If Pydantic is needed but not installed
|
|
132
|
+
"""
|
|
133
|
+
# If it's already a dict, return it
|
|
134
|
+
if isinstance(schema, dict):
|
|
135
|
+
return schema
|
|
136
|
+
|
|
137
|
+
# If it's a Pydantic model, convert it
|
|
138
|
+
if is_pydantic_model(schema):
|
|
139
|
+
return model_to_schema(schema)
|
|
140
|
+
|
|
141
|
+
# Otherwise, raise an error
|
|
142
|
+
raise TypeError(
|
|
143
|
+
f"Expected JSON Schema dict or Pydantic BaseModel, got {type(schema).__name__}"
|
|
144
|
+
)
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session API for applefoundationmodels Python bindings.
|
|
3
|
+
|
|
4
|
+
Provides session management, text generation, and async streaming support.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from typing import (
|
|
11
|
+
Optional,
|
|
12
|
+
Dict,
|
|
13
|
+
Any,
|
|
14
|
+
Callable,
|
|
15
|
+
Union,
|
|
16
|
+
TYPE_CHECKING,
|
|
17
|
+
List,
|
|
18
|
+
cast,
|
|
19
|
+
Iterator,
|
|
20
|
+
overload,
|
|
21
|
+
Type,
|
|
22
|
+
)
|
|
23
|
+
from typing_extensions import Literal
|
|
24
|
+
from queue import Queue, Empty
|
|
25
|
+
import threading
|
|
26
|
+
|
|
27
|
+
from .base_session import BaseSession, StreamQueueItem
|
|
28
|
+
from .types import (
|
|
29
|
+
GenerationResponse,
|
|
30
|
+
StreamChunk,
|
|
31
|
+
)
|
|
32
|
+
from .pydantic_compat import normalize_schema
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from pydantic import BaseModel
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Session(BaseSession):
|
|
41
|
+
"""
|
|
42
|
+
AI session for maintaining conversation state.
|
|
43
|
+
|
|
44
|
+
Sessions maintain conversation history and can be configured with tools
|
|
45
|
+
and instructions. Use as a context manager for automatic cleanup.
|
|
46
|
+
|
|
47
|
+
Usage:
|
|
48
|
+
with Session() as session:
|
|
49
|
+
response = session.generate("Hello!")
|
|
50
|
+
print(response.text)
|
|
51
|
+
|
|
52
|
+
# With configuration:
|
|
53
|
+
def get_weather(location: str) -> str:
|
|
54
|
+
'''Get current weather for a location.'''
|
|
55
|
+
return f"Weather in {location}: 22°C"
|
|
56
|
+
|
|
57
|
+
session = Session(
|
|
58
|
+
instructions="You are a helpful assistant.",
|
|
59
|
+
tools=[get_weather]
|
|
60
|
+
)
|
|
61
|
+
response = session.generate("What's the weather in Paris?")
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def _call_ffi(self, func, *args, **kwargs):
|
|
65
|
+
"""Execute FFI call synchronously."""
|
|
66
|
+
return func(*args, **kwargs)
|
|
67
|
+
|
|
68
|
+
def _create_stream_queue_adapter(self) -> BaseSession._StreamQueueAdapter:
|
|
69
|
+
"""Return the queue adapter used by synchronous streaming."""
|
|
70
|
+
queue: Queue[StreamQueueItem] = Queue()
|
|
71
|
+
|
|
72
|
+
def push(item: StreamQueueItem) -> None:
|
|
73
|
+
queue.put(item)
|
|
74
|
+
|
|
75
|
+
def get_sync() -> StreamQueueItem:
|
|
76
|
+
while True:
|
|
77
|
+
try:
|
|
78
|
+
return queue.get(timeout=0.1)
|
|
79
|
+
except Empty:
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
return BaseSession._StreamQueueAdapter(push=push, get_sync=get_sync)
|
|
83
|
+
|
|
84
|
+
def close(self) -> None:
|
|
85
|
+
"""Close the session and cleanup resources."""
|
|
86
|
+
self._mark_closed()
|
|
87
|
+
|
|
88
|
+
# ========================================================================
|
|
89
|
+
# Type overloads for generate() method
|
|
90
|
+
#
|
|
91
|
+
# IMPORTANT: These overloads must be kept in sync with AsyncSession.generate()
|
|
92
|
+
# in async_session.py. The signatures are identical except for:
|
|
93
|
+
# - async keyword (Session: def generate() vs AsyncSession: async def generate())
|
|
94
|
+
# - Return type for streaming (Iterator vs AsyncIterator)
|
|
95
|
+
#
|
|
96
|
+
# When modifying these overloads, update both files to maintain consistency.
|
|
97
|
+
# ========================================================================
|
|
98
|
+
|
|
99
|
+
# Type overload for non-streaming text generation
|
|
100
|
+
@overload
|
|
101
|
+
def generate(
|
|
102
|
+
self,
|
|
103
|
+
prompt: str,
|
|
104
|
+
schema: None = None,
|
|
105
|
+
stream: Literal[False] = False,
|
|
106
|
+
temperature: Optional[float] = None,
|
|
107
|
+
max_tokens: Optional[int] = None,
|
|
108
|
+
) -> GenerationResponse: ...
|
|
109
|
+
|
|
110
|
+
# Type overload for non-streaming structured generation
|
|
111
|
+
@overload
|
|
112
|
+
def generate(
|
|
113
|
+
self,
|
|
114
|
+
prompt: str,
|
|
115
|
+
schema: Union[Dict[str, Any], Type["BaseModel"]],
|
|
116
|
+
stream: Literal[False] = False,
|
|
117
|
+
temperature: Optional[float] = None,
|
|
118
|
+
max_tokens: Optional[int] = None,
|
|
119
|
+
) -> GenerationResponse: ...
|
|
120
|
+
|
|
121
|
+
# Type overload for streaming generation (text only, no structured streaming)
|
|
122
|
+
@overload
|
|
123
|
+
def generate(
|
|
124
|
+
self,
|
|
125
|
+
prompt: str,
|
|
126
|
+
schema: None = None,
|
|
127
|
+
stream: Literal[True] = True,
|
|
128
|
+
temperature: Optional[float] = None,
|
|
129
|
+
max_tokens: Optional[int] = None,
|
|
130
|
+
) -> Iterator[StreamChunk]: ...
|
|
131
|
+
|
|
132
|
+
def generate(
|
|
133
|
+
self,
|
|
134
|
+
prompt: str,
|
|
135
|
+
schema: Optional[Union[Dict[str, Any], Type["BaseModel"]]] = None,
|
|
136
|
+
stream: bool = False,
|
|
137
|
+
temperature: Optional[float] = None,
|
|
138
|
+
max_tokens: Optional[int] = None,
|
|
139
|
+
) -> Union[GenerationResponse, Iterator[StreamChunk]]:
|
|
140
|
+
"""
|
|
141
|
+
Generate text or structured output, with optional streaming.
|
|
142
|
+
|
|
143
|
+
This unified method supports three generation modes:
|
|
144
|
+
1. Text generation (schema=None, stream=False) -> GenerationResponse
|
|
145
|
+
2. Structured generation (schema=dict/model, stream=False) -> GenerationResponse
|
|
146
|
+
3. Streaming generation (schema=None, stream=True) -> Iterator[StreamChunk]
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
prompt: Input text prompt
|
|
150
|
+
schema: Optional JSON schema dict or Pydantic model for structured output
|
|
151
|
+
stream: If True, return an iterator of chunks instead of complete response
|
|
152
|
+
temperature: Sampling temperature (0.0-2.0, default: DEFAULT_TEMPERATURE)
|
|
153
|
+
max_tokens: Maximum tokens to generate (default: DEFAULT_MAX_TOKENS)
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
- GenerationResponse with .text or .parsed property (if stream=False)
|
|
157
|
+
- Iterator[StreamChunk] yielding content deltas (if stream=True)
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
RuntimeError: If session is closed
|
|
161
|
+
GenerationError: If generation fails
|
|
162
|
+
ValueError: If schema is provided with stream=True
|
|
163
|
+
|
|
164
|
+
Examples:
|
|
165
|
+
Text generation:
|
|
166
|
+
>>> response = session.generate("What is Python?")
|
|
167
|
+
>>> print(response.text)
|
|
168
|
+
|
|
169
|
+
Structured generation:
|
|
170
|
+
>>> schema = {"type": "object", "properties": {"name": {"type": "string"}}}
|
|
171
|
+
>>> response = session.generate("Extract name: John Doe", schema=schema)
|
|
172
|
+
>>> print(response.parsed)
|
|
173
|
+
|
|
174
|
+
Streaming generation:
|
|
175
|
+
>>> for chunk in session.generate("Tell me a story", stream=True):
|
|
176
|
+
... print(chunk.content, end='', flush=True)
|
|
177
|
+
"""
|
|
178
|
+
plan = self._plan_generate_call(stream, schema, temperature, max_tokens)
|
|
179
|
+
|
|
180
|
+
if plan.mode == "stream":
|
|
181
|
+
# Streaming mode: return Iterator[StreamChunk]
|
|
182
|
+
return self._generate_stream_impl(prompt, plan.temperature, plan.max_tokens)
|
|
183
|
+
if plan.mode == "structured" and schema is not None:
|
|
184
|
+
# Structured generation mode
|
|
185
|
+
return self._generate_structured_impl(
|
|
186
|
+
prompt, schema, plan.temperature, plan.max_tokens
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Text generation mode
|
|
190
|
+
return self._generate_text_impl(prompt, plan.temperature, plan.max_tokens)
|
|
191
|
+
|
|
192
|
+
def _generate_text_impl(
|
|
193
|
+
self, prompt: str, temperature: float, max_tokens: int
|
|
194
|
+
) -> GenerationResponse:
|
|
195
|
+
"""Internal implementation for text generation."""
|
|
196
|
+
with self._generation_context() as start_length:
|
|
197
|
+
text = self._call_ffi(
|
|
198
|
+
self._ffi.generate,
|
|
199
|
+
prompt,
|
|
200
|
+
temperature,
|
|
201
|
+
max_tokens,
|
|
202
|
+
)
|
|
203
|
+
return self._build_generation_response(text, False, start_length)
|
|
204
|
+
|
|
205
|
+
def _generate_structured_impl(
|
|
206
|
+
self,
|
|
207
|
+
prompt: str,
|
|
208
|
+
schema: Union[Dict[str, Any], Type["BaseModel"]],
|
|
209
|
+
temperature: float,
|
|
210
|
+
max_tokens: int,
|
|
211
|
+
) -> GenerationResponse:
|
|
212
|
+
"""Internal implementation for structured generation."""
|
|
213
|
+
with self._generation_context() as start_length:
|
|
214
|
+
json_schema = normalize_schema(schema)
|
|
215
|
+
result = self._call_ffi(
|
|
216
|
+
self._ffi.generate_structured,
|
|
217
|
+
prompt,
|
|
218
|
+
json_schema,
|
|
219
|
+
temperature,
|
|
220
|
+
max_tokens,
|
|
221
|
+
)
|
|
222
|
+
return self._build_generation_response(result, True, start_length)
|
|
223
|
+
|
|
224
|
+
def _generate_stream_impl(
|
|
225
|
+
self, prompt: str, temperature: float, max_tokens: int
|
|
226
|
+
) -> Iterator[StreamChunk]:
|
|
227
|
+
"""Internal implementation for streaming generation."""
|
|
228
|
+
start_length = self._begin_generation()
|
|
229
|
+
adapter = self._create_stream_queue_adapter()
|
|
230
|
+
try:
|
|
231
|
+
# Use shared streaming implementation from base class
|
|
232
|
+
yield from self._stream_chunks_impl(
|
|
233
|
+
prompt, temperature, max_tokens, adapter
|
|
234
|
+
)
|
|
235
|
+
finally:
|
|
236
|
+
self._end_generation(start_length)
|
|
237
|
+
|
|
238
|
+
def get_history(self) -> List[Dict[str, Any]]:
|
|
239
|
+
"""
|
|
240
|
+
Get conversation history.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
List of message dictionaries with 'role' and 'content' keys
|
|
244
|
+
|
|
245
|
+
Example:
|
|
246
|
+
>>> history = session.get_history()
|
|
247
|
+
>>> for msg in history:
|
|
248
|
+
... print(f"{msg['role']}: {msg['content']}")
|
|
249
|
+
"""
|
|
250
|
+
self._check_closed()
|
|
251
|
+
result = self._call_ffi(self._ffi.get_history)
|
|
252
|
+
return cast(List[Dict[str, Any]], result)
|
|
253
|
+
|
|
254
|
+
def clear_history(self) -> None:
|
|
255
|
+
"""
|
|
256
|
+
Clear conversation history.
|
|
257
|
+
|
|
258
|
+
Removes all messages from the session while keeping the session active.
|
|
259
|
+
"""
|
|
260
|
+
self._check_closed()
|
|
261
|
+
self._call_ffi(self._ffi.clear_history)
|
|
262
|
+
# Reset to current transcript length (may include persistent instructions)
|
|
263
|
+
self._last_transcript_length = len(self.transcript)
|
|
264
|
+
|
|
265
|
+
# Properties inherited from BaseSession (transcript, last_generation_transcript)
|