huitzo-sdk 0.0.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.
- huitzo_sdk/__init__.py +85 -0
- huitzo_sdk/command.py +180 -0
- huitzo_sdk/context.py +122 -0
- huitzo_sdk/errors.py +241 -0
- huitzo_sdk/integrations/__init__.py +26 -0
- huitzo_sdk/integrations/email.py +109 -0
- huitzo_sdk/integrations/files.py +188 -0
- huitzo_sdk/integrations/http.py +173 -0
- huitzo_sdk/integrations/llm.py +157 -0
- huitzo_sdk/integrations/telegram.py +117 -0
- huitzo_sdk/storage/__init__.py +21 -0
- huitzo_sdk/storage/memory.py +181 -0
- huitzo_sdk/storage/namespace.py +64 -0
- huitzo_sdk/storage/protocol.py +173 -0
- huitzo_sdk/types.py +47 -0
- huitzo_sdk-0.0.0.dist-info/METADATA +12 -0
- huitzo_sdk-0.0.0.dist-info/RECORD +18 -0
- huitzo_sdk-0.0.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module: integrations.email
|
|
3
|
+
Description: Email integration client for sending emails and templates.
|
|
4
|
+
|
|
5
|
+
Implements:
|
|
6
|
+
- docs/sdk/integrations.md#email-integration
|
|
7
|
+
|
|
8
|
+
See Also:
|
|
9
|
+
- docs/sdk/error-handling.md (for EmailError)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Any, Protocol, runtime_checkable
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@runtime_checkable
|
|
19
|
+
class EmailBackend(Protocol):
|
|
20
|
+
"""Backend protocol for email providers."""
|
|
21
|
+
|
|
22
|
+
async def send(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
to: str | list[str],
|
|
26
|
+
subject: str,
|
|
27
|
+
body: str | None,
|
|
28
|
+
html: str | None,
|
|
29
|
+
cc: list[str] | None,
|
|
30
|
+
bcc: list[str] | None,
|
|
31
|
+
attachments: list[dict[str, Any]] | None,
|
|
32
|
+
) -> None: ...
|
|
33
|
+
|
|
34
|
+
async def send_template(
|
|
35
|
+
self,
|
|
36
|
+
*,
|
|
37
|
+
to: str | list[str],
|
|
38
|
+
template_id: str,
|
|
39
|
+
template_data: dict[str, Any] | None,
|
|
40
|
+
) -> None: ...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class EmailClient:
|
|
45
|
+
"""Client for sending emails.
|
|
46
|
+
|
|
47
|
+
This is the SDK-side interface. The backend injects a real implementation
|
|
48
|
+
at runtime (SendGrid, SMTP, etc.).
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
_backend: EmailBackend
|
|
52
|
+
|
|
53
|
+
async def send(
|
|
54
|
+
self,
|
|
55
|
+
*,
|
|
56
|
+
to: str | list[str],
|
|
57
|
+
subject: str,
|
|
58
|
+
body: str | None = None,
|
|
59
|
+
html: str | None = None,
|
|
60
|
+
cc: list[str] | None = None,
|
|
61
|
+
bcc: list[str] | None = None,
|
|
62
|
+
attachments: list[dict[str, Any]] | None = None,
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Send an email.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
to: Recipient(s).
|
|
68
|
+
subject: Email subject.
|
|
69
|
+
body: Plain text body.
|
|
70
|
+
html: HTML body.
|
|
71
|
+
cc: CC recipients.
|
|
72
|
+
bcc: BCC recipients.
|
|
73
|
+
attachments: List of attachment dicts with filename, content, content_type.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
EmailError: If sending fails.
|
|
77
|
+
"""
|
|
78
|
+
await self._backend.send(
|
|
79
|
+
to=to,
|
|
80
|
+
subject=subject,
|
|
81
|
+
body=body,
|
|
82
|
+
html=html,
|
|
83
|
+
cc=cc,
|
|
84
|
+
bcc=bcc,
|
|
85
|
+
attachments=attachments,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
async def send_template(
|
|
89
|
+
self,
|
|
90
|
+
*,
|
|
91
|
+
to: str | list[str],
|
|
92
|
+
template_id: str,
|
|
93
|
+
template_data: dict[str, Any] | None = None,
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Send a templated email.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
to: Recipient(s).
|
|
99
|
+
template_id: Provider template ID.
|
|
100
|
+
template_data: Template variable substitutions.
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
EmailError: If sending fails.
|
|
104
|
+
"""
|
|
105
|
+
await self._backend.send_template(
|
|
106
|
+
to=to,
|
|
107
|
+
template_id=template_id,
|
|
108
|
+
template_data=template_data,
|
|
109
|
+
)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module: integrations.files
|
|
3
|
+
Description: File integration client for reading and writing files with backend abstraction.
|
|
4
|
+
|
|
5
|
+
Implements:
|
|
6
|
+
- docs/sdk/integrations.md#files-integration
|
|
7
|
+
|
|
8
|
+
See Also:
|
|
9
|
+
- docs/sdk/context.md (for Context wiring)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import builtins
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any, Protocol, runtime_checkable
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@runtime_checkable
|
|
20
|
+
class FileStorageBackend(Protocol):
|
|
21
|
+
"""Backend protocol for file storage (local, S3, Azure, GCS)."""
|
|
22
|
+
|
|
23
|
+
async def read(self, path: str) -> bytes: ...
|
|
24
|
+
|
|
25
|
+
async def write(self, path: str, content: bytes) -> None: ...
|
|
26
|
+
|
|
27
|
+
async def info(self, path: str) -> dict[str, Any]: ...
|
|
28
|
+
|
|
29
|
+
async def list(self, prefix: str) -> builtins.list[dict[str, Any]]: ...
|
|
30
|
+
|
|
31
|
+
async def exists(self, path: str) -> bool: ...
|
|
32
|
+
|
|
33
|
+
async def get_url(self, path: str, *, expires: int) -> str: ...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class FileClient:
|
|
38
|
+
"""Client for reading and writing files.
|
|
39
|
+
|
|
40
|
+
Backend-agnostic: works with local filesystem, S3, Azure Blob, or GCS.
|
|
41
|
+
Tenant path isolation is handled automatically by the backend.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
_backend: FileStorageBackend
|
|
45
|
+
|
|
46
|
+
async def read_excel(
|
|
47
|
+
self,
|
|
48
|
+
path: str,
|
|
49
|
+
*,
|
|
50
|
+
sheet: str | None = None,
|
|
51
|
+
) -> Any:
|
|
52
|
+
"""Read an Excel file and return a DataFrame.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
path: Relative file path.
|
|
56
|
+
sheet: Specific sheet name (default: first sheet).
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
pandas DataFrame.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ImportError: If pandas is not installed.
|
|
63
|
+
IntegrationError: If the backend read fails.
|
|
64
|
+
"""
|
|
65
|
+
data = await self._backend.read(path)
|
|
66
|
+
import io
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
import pandas as pd
|
|
70
|
+
except ImportError as exc:
|
|
71
|
+
raise ImportError("pandas is required for read_excel") from exc
|
|
72
|
+
kwargs: dict[str, Any] = {}
|
|
73
|
+
if sheet is not None:
|
|
74
|
+
kwargs["sheet_name"] = sheet
|
|
75
|
+
return pd.read_excel(io.BytesIO(data), **kwargs)
|
|
76
|
+
|
|
77
|
+
async def read_csv(
|
|
78
|
+
self,
|
|
79
|
+
path: str,
|
|
80
|
+
*,
|
|
81
|
+
delimiter: str = ",",
|
|
82
|
+
encoding: str = "utf-8",
|
|
83
|
+
) -> Any:
|
|
84
|
+
"""Read a CSV file and return a DataFrame.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
path: Relative file path.
|
|
88
|
+
delimiter: Column delimiter.
|
|
89
|
+
encoding: File encoding.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
pandas DataFrame.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
ImportError: If pandas is not installed.
|
|
96
|
+
IntegrationError: If the backend read fails.
|
|
97
|
+
"""
|
|
98
|
+
data = await self._backend.read(path)
|
|
99
|
+
import io
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
import pandas as pd
|
|
103
|
+
except ImportError as exc:
|
|
104
|
+
raise ImportError("pandas is required for read_csv") from exc
|
|
105
|
+
return pd.read_csv(io.BytesIO(data), delimiter=delimiter, encoding=encoding)
|
|
106
|
+
|
|
107
|
+
async def read_json(self, path: str) -> Any:
|
|
108
|
+
"""Read a JSON file.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
path: Relative file path.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Parsed JSON data.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
IntegrationError: If the backend read fails.
|
|
118
|
+
json.JSONDecodeError: If file content is not valid JSON.
|
|
119
|
+
"""
|
|
120
|
+
import json
|
|
121
|
+
|
|
122
|
+
data = await self._backend.read(path)
|
|
123
|
+
return json.loads(data)
|
|
124
|
+
|
|
125
|
+
async def write(
|
|
126
|
+
self,
|
|
127
|
+
path: str,
|
|
128
|
+
content: str | bytes,
|
|
129
|
+
*,
|
|
130
|
+
binary: bool = False,
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Write content to a file.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
path: Relative file path.
|
|
136
|
+
content: File content (string or bytes).
|
|
137
|
+
binary: If True, content is bytes.
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
IntegrationError: If the backend write fails.
|
|
141
|
+
"""
|
|
142
|
+
if isinstance(content, str):
|
|
143
|
+
raw = content.encode("utf-8")
|
|
144
|
+
else:
|
|
145
|
+
raw = content
|
|
146
|
+
await self._backend.write(path, raw)
|
|
147
|
+
|
|
148
|
+
async def info(self, path: str) -> dict[str, Any]:
|
|
149
|
+
"""Get file metadata.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
path: Relative file path.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Dict with size, created, modified, type.
|
|
156
|
+
"""
|
|
157
|
+
return await self._backend.info(path)
|
|
158
|
+
|
|
159
|
+
async def list(self, prefix: str = "") -> builtins.list[dict[str, Any]]:
|
|
160
|
+
"""List files matching a prefix.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
prefix: Path prefix (e.g. "uploads/").
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
List of file info dicts.
|
|
167
|
+
"""
|
|
168
|
+
return await self._backend.list(prefix)
|
|
169
|
+
|
|
170
|
+
async def exists(self, path: str) -> bool:
|
|
171
|
+
"""Check if a file exists.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
path: Relative file path.
|
|
175
|
+
"""
|
|
176
|
+
return await self._backend.exists(path)
|
|
177
|
+
|
|
178
|
+
async def get_url(self, path: str, *, expires: int = 3600) -> str:
|
|
179
|
+
"""Generate a download URL for a file.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
path: Relative file path.
|
|
183
|
+
expires: URL expiration in seconds.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Presigned download URL.
|
|
187
|
+
"""
|
|
188
|
+
return await self._backend.get_url(path, expires=expires)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module: integrations.http
|
|
3
|
+
Description: HTTP integration client for external API requests with domain restrictions.
|
|
4
|
+
|
|
5
|
+
Implements:
|
|
6
|
+
- docs/sdk/integrations.md#http-integration
|
|
7
|
+
|
|
8
|
+
See Also:
|
|
9
|
+
- docs/sdk/error-handling.md (for HTTPError, HTTPSecurityError)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any, Protocol, runtime_checkable
|
|
16
|
+
from urllib.parse import urlparse
|
|
17
|
+
|
|
18
|
+
from huitzo_sdk.errors import HTTPSecurityError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@runtime_checkable
|
|
22
|
+
class HTTPBackend(Protocol):
|
|
23
|
+
"""Backend protocol for HTTP requests."""
|
|
24
|
+
|
|
25
|
+
async def request(
|
|
26
|
+
self,
|
|
27
|
+
method: str,
|
|
28
|
+
url: str,
|
|
29
|
+
*,
|
|
30
|
+
headers: dict[str, str] | None,
|
|
31
|
+
params: dict[str, Any] | None,
|
|
32
|
+
json: Any | None,
|
|
33
|
+
data: dict[str, Any] | None,
|
|
34
|
+
files: dict[str, bytes] | None,
|
|
35
|
+
timeout: int | None,
|
|
36
|
+
) -> Any: ...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class HTTPClient:
|
|
41
|
+
"""Client for making HTTP requests to external APIs.
|
|
42
|
+
|
|
43
|
+
Supports domain restrictions to limit which external services
|
|
44
|
+
a pack can communicate with.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
_backend: HTTPBackend
|
|
48
|
+
_allowed_domains: list[str] = field(default_factory=list)
|
|
49
|
+
|
|
50
|
+
def _check_domain(self, url: str) -> None:
|
|
51
|
+
"""Validate the URL domain against allowed domains."""
|
|
52
|
+
if not self._allowed_domains:
|
|
53
|
+
return
|
|
54
|
+
parsed = urlparse(url)
|
|
55
|
+
domain = parsed.hostname or ""
|
|
56
|
+
for allowed in self._allowed_domains:
|
|
57
|
+
if allowed.startswith("*."):
|
|
58
|
+
base = allowed[2:] # e.g. "trusted-domain.org"
|
|
59
|
+
if domain.endswith("." + base):
|
|
60
|
+
return
|
|
61
|
+
elif domain == allowed:
|
|
62
|
+
return
|
|
63
|
+
raise HTTPSecurityError(domain=domain)
|
|
64
|
+
|
|
65
|
+
async def get(
|
|
66
|
+
self,
|
|
67
|
+
url: str,
|
|
68
|
+
*,
|
|
69
|
+
headers: dict[str, str] | None = None,
|
|
70
|
+
params: dict[str, Any] | None = None,
|
|
71
|
+
timeout: int | None = None,
|
|
72
|
+
) -> Any:
|
|
73
|
+
"""Send a GET request.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
HTTPError: If the request fails.
|
|
77
|
+
HTTPSecurityError: If domain not allowed.
|
|
78
|
+
"""
|
|
79
|
+
self._check_domain(url)
|
|
80
|
+
return await self._backend.request(
|
|
81
|
+
"GET",
|
|
82
|
+
url,
|
|
83
|
+
headers=headers,
|
|
84
|
+
params=params,
|
|
85
|
+
json=None,
|
|
86
|
+
data=None,
|
|
87
|
+
files=None,
|
|
88
|
+
timeout=timeout,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
async def post(
|
|
92
|
+
self,
|
|
93
|
+
url: str,
|
|
94
|
+
*,
|
|
95
|
+
headers: dict[str, str] | None = None,
|
|
96
|
+
params: dict[str, Any] | None = None,
|
|
97
|
+
json: Any | None = None,
|
|
98
|
+
data: dict[str, Any] | None = None,
|
|
99
|
+
files: dict[str, bytes] | None = None,
|
|
100
|
+
timeout: int | None = None,
|
|
101
|
+
) -> Any:
|
|
102
|
+
"""Send a POST request.
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
HTTPError: If the request fails.
|
|
106
|
+
HTTPSecurityError: If domain not allowed.
|
|
107
|
+
"""
|
|
108
|
+
self._check_domain(url)
|
|
109
|
+
return await self._backend.request(
|
|
110
|
+
"POST",
|
|
111
|
+
url,
|
|
112
|
+
headers=headers,
|
|
113
|
+
params=params,
|
|
114
|
+
json=json,
|
|
115
|
+
data=data,
|
|
116
|
+
files=files,
|
|
117
|
+
timeout=timeout,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
async def put(
|
|
121
|
+
self,
|
|
122
|
+
url: str,
|
|
123
|
+
*,
|
|
124
|
+
headers: dict[str, str] | None = None,
|
|
125
|
+
params: dict[str, Any] | None = None,
|
|
126
|
+
json: Any | None = None,
|
|
127
|
+
data: dict[str, Any] | None = None,
|
|
128
|
+
files: dict[str, bytes] | None = None,
|
|
129
|
+
timeout: int | None = None,
|
|
130
|
+
) -> Any:
|
|
131
|
+
"""Send a PUT request.
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
HTTPError: If the request fails.
|
|
135
|
+
HTTPSecurityError: If domain not allowed.
|
|
136
|
+
"""
|
|
137
|
+
self._check_domain(url)
|
|
138
|
+
return await self._backend.request(
|
|
139
|
+
"PUT",
|
|
140
|
+
url,
|
|
141
|
+
headers=headers,
|
|
142
|
+
params=params,
|
|
143
|
+
json=json,
|
|
144
|
+
data=data,
|
|
145
|
+
files=files,
|
|
146
|
+
timeout=timeout,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
async def delete(
|
|
150
|
+
self,
|
|
151
|
+
url: str,
|
|
152
|
+
*,
|
|
153
|
+
headers: dict[str, str] | None = None,
|
|
154
|
+
params: dict[str, Any] | None = None,
|
|
155
|
+
timeout: int | None = None,
|
|
156
|
+
) -> Any:
|
|
157
|
+
"""Send a DELETE request.
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
HTTPError: If the request fails.
|
|
161
|
+
HTTPSecurityError: If domain not allowed.
|
|
162
|
+
"""
|
|
163
|
+
self._check_domain(url)
|
|
164
|
+
return await self._backend.request(
|
|
165
|
+
"DELETE",
|
|
166
|
+
url,
|
|
167
|
+
headers=headers,
|
|
168
|
+
params=params,
|
|
169
|
+
json=None,
|
|
170
|
+
data=None,
|
|
171
|
+
files=None,
|
|
172
|
+
timeout=timeout,
|
|
173
|
+
)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module: integrations.llm
|
|
3
|
+
Description: LLM integration client for AI completions and streaming.
|
|
4
|
+
|
|
5
|
+
Implements:
|
|
6
|
+
- docs/sdk/integrations.md#llm-integration
|
|
7
|
+
|
|
8
|
+
See Also:
|
|
9
|
+
- docs/sdk/error-handling.md (for LLMError)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any, AsyncIterator, Protocol, runtime_checkable
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@runtime_checkable
|
|
19
|
+
class LLMBackend(Protocol):
|
|
20
|
+
"""Backend protocol for LLM providers."""
|
|
21
|
+
|
|
22
|
+
async def complete(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
prompt: str,
|
|
26
|
+
system: str | None,
|
|
27
|
+
model: str,
|
|
28
|
+
temperature: float,
|
|
29
|
+
max_tokens: int | None,
|
|
30
|
+
top_p: float | None,
|
|
31
|
+
stop: list[str] | None,
|
|
32
|
+
response_format: str | None,
|
|
33
|
+
schema: type[Any] | None,
|
|
34
|
+
) -> str: ...
|
|
35
|
+
|
|
36
|
+
def stream(
|
|
37
|
+
self,
|
|
38
|
+
*,
|
|
39
|
+
prompt: str,
|
|
40
|
+
system: str | None,
|
|
41
|
+
model: str,
|
|
42
|
+
temperature: float,
|
|
43
|
+
max_tokens: int | None,
|
|
44
|
+
top_p: float | None,
|
|
45
|
+
stop: list[str] | None,
|
|
46
|
+
) -> AsyncIterator[str]: ...
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class TokenUsage:
|
|
51
|
+
"""Token usage tracking for a single LLM call."""
|
|
52
|
+
|
|
53
|
+
prompt_tokens: int = 0
|
|
54
|
+
completion_tokens: int = 0
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def total_tokens(self) -> int:
|
|
58
|
+
return self.prompt_tokens + self.completion_tokens
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class LLMClient:
|
|
63
|
+
"""Client for LLM completions and streaming.
|
|
64
|
+
|
|
65
|
+
This is the SDK-side interface. The backend injects a real implementation
|
|
66
|
+
at runtime that handles actual API calls.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
_backend: LLMBackend
|
|
70
|
+
_usage: list[TokenUsage] = field(default_factory=list, init=False)
|
|
71
|
+
|
|
72
|
+
async def complete(
|
|
73
|
+
self,
|
|
74
|
+
prompt: str,
|
|
75
|
+
*,
|
|
76
|
+
model: str = "gpt-4o-mini",
|
|
77
|
+
system: str | None = None,
|
|
78
|
+
temperature: float = 1.0,
|
|
79
|
+
max_tokens: int | None = None,
|
|
80
|
+
top_p: float | None = None,
|
|
81
|
+
stop: list[str] | None = None,
|
|
82
|
+
response_format: str | None = None,
|
|
83
|
+
schema: type[Any] | None = None,
|
|
84
|
+
) -> str:
|
|
85
|
+
"""Generate a completion from an LLM.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
prompt: The user prompt.
|
|
89
|
+
model: Model identifier (e.g. "gpt-4o", "claude-3-5-sonnet").
|
|
90
|
+
system: Optional system message.
|
|
91
|
+
temperature: Sampling temperature (0.0-2.0).
|
|
92
|
+
max_tokens: Maximum response tokens.
|
|
93
|
+
top_p: Nucleus sampling parameter.
|
|
94
|
+
stop: Stop sequences.
|
|
95
|
+
response_format: "json" for structured output.
|
|
96
|
+
schema: Pydantic model for JSON mode validation.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
The completion text.
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
LLMError: If the LLM call fails.
|
|
103
|
+
"""
|
|
104
|
+
return await self._backend.complete(
|
|
105
|
+
prompt=prompt,
|
|
106
|
+
system=system,
|
|
107
|
+
model=model,
|
|
108
|
+
temperature=temperature,
|
|
109
|
+
max_tokens=max_tokens,
|
|
110
|
+
top_p=top_p,
|
|
111
|
+
stop=stop,
|
|
112
|
+
response_format=response_format,
|
|
113
|
+
schema=schema,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def stream(
|
|
117
|
+
self,
|
|
118
|
+
prompt: str,
|
|
119
|
+
*,
|
|
120
|
+
model: str = "gpt-4o-mini",
|
|
121
|
+
system: str | None = None,
|
|
122
|
+
temperature: float = 1.0,
|
|
123
|
+
max_tokens: int | None = None,
|
|
124
|
+
top_p: float | None = None,
|
|
125
|
+
stop: list[str] | None = None,
|
|
126
|
+
) -> AsyncIterator[str]:
|
|
127
|
+
"""Stream a completion from an LLM.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
prompt: The user prompt.
|
|
131
|
+
model: Model identifier.
|
|
132
|
+
system: Optional system message.
|
|
133
|
+
temperature: Sampling temperature.
|
|
134
|
+
max_tokens: Maximum response tokens.
|
|
135
|
+
top_p: Nucleus sampling parameter.
|
|
136
|
+
stop: Stop sequences.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
An async iterator yielding text chunks.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
LLMError: If the LLM call fails.
|
|
143
|
+
"""
|
|
144
|
+
return self._backend.stream(
|
|
145
|
+
prompt=prompt,
|
|
146
|
+
system=system,
|
|
147
|
+
model=model,
|
|
148
|
+
temperature=temperature,
|
|
149
|
+
max_tokens=max_tokens,
|
|
150
|
+
top_p=top_p,
|
|
151
|
+
stop=stop,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def usage(self) -> list[TokenUsage]:
|
|
156
|
+
"""Token usage records for this client."""
|
|
157
|
+
return list(self._usage)
|