vedika-agent-sdk 0.2.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.
- vedika_agent/__init__.py +65 -0
- vedika_agent/_min_sdk.py +138 -0
- vedika_agent/client.py +401 -0
- vedika_agent/errors.py +155 -0
- vedika_agent/py.typed +0 -0
- vedika_agent/stream.py +391 -0
- vedika_agent/types.py +230 -0
- vedika_agent_sdk-0.2.0.dist-info/METADATA +161 -0
- vedika_agent_sdk-0.2.0.dist-info/RECORD +12 -0
- vedika_agent_sdk-0.2.0.dist-info/WHEEL +5 -0
- vedika_agent_sdk-0.2.0.dist-info/licenses/LICENSE +17 -0
- vedika_agent_sdk-0.2.0.dist-info/top_level.txt +1 -0
vedika_agent/__init__.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Public entry point for the Vedika agent SDK."""
|
|
2
|
+
|
|
3
|
+
from .client import Transport, VedikaAgent, create_agent
|
|
4
|
+
from .errors import (
|
|
5
|
+
AuthenticationError,
|
|
6
|
+
InvalidRequestError,
|
|
7
|
+
NotFoundError,
|
|
8
|
+
ProtocolError,
|
|
9
|
+
RateLimitError,
|
|
10
|
+
ServerError,
|
|
11
|
+
StreamError,
|
|
12
|
+
VedikaAgentError,
|
|
13
|
+
is_vedika_agent_error,
|
|
14
|
+
)
|
|
15
|
+
from .stream import (
|
|
16
|
+
Turn,
|
|
17
|
+
format_last_event_id,
|
|
18
|
+
parse_chunk,
|
|
19
|
+
parse_sse_chunk,
|
|
20
|
+
UI_MESSAGE_STREAM_HEADER,
|
|
21
|
+
)
|
|
22
|
+
from .types import (
|
|
23
|
+
Artifact,
|
|
24
|
+
CancelTurnOptions,
|
|
25
|
+
CreateThreadOptions,
|
|
26
|
+
Json,
|
|
27
|
+
MemoryToggles,
|
|
28
|
+
ResumeTurnOptions,
|
|
29
|
+
SendTurnOptions,
|
|
30
|
+
Thread,
|
|
31
|
+
TurnInput,
|
|
32
|
+
TurnOutcome,
|
|
33
|
+
UIMessageStreamChunk,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"VedikaAgent",
|
|
38
|
+
"create_agent",
|
|
39
|
+
"Transport",
|
|
40
|
+
"VedikaAgentError",
|
|
41
|
+
"AuthenticationError",
|
|
42
|
+
"NotFoundError",
|
|
43
|
+
"RateLimitError",
|
|
44
|
+
"InvalidRequestError",
|
|
45
|
+
"ServerError",
|
|
46
|
+
"StreamError",
|
|
47
|
+
"ProtocolError",
|
|
48
|
+
"is_vedika_agent_error",
|
|
49
|
+
"parse_chunk",
|
|
50
|
+
"parse_sse_chunk",
|
|
51
|
+
"format_last_event_id",
|
|
52
|
+
"UI_MESSAGE_STREAM_HEADER",
|
|
53
|
+
"Artifact",
|
|
54
|
+
"CancelTurnOptions",
|
|
55
|
+
"CreateThreadOptions",
|
|
56
|
+
"Json",
|
|
57
|
+
"MemoryToggles",
|
|
58
|
+
"ResumeTurnOptions",
|
|
59
|
+
"SendTurnOptions",
|
|
60
|
+
"Thread",
|
|
61
|
+
"Turn",
|
|
62
|
+
"TurnInput",
|
|
63
|
+
"TurnOutcome",
|
|
64
|
+
"UIMessageStreamChunk",
|
|
65
|
+
]
|
vedika_agent/_min_sdk.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""The "server says your SDK is too old" warning.
|
|
2
|
+
|
|
3
|
+
Every harness response may carry ``X-Harness-Min-Sdk: <major.minor.patch>``.
|
|
4
|
+
When this SDK's own version is lower, one ``SdkVersionWarning`` is emitted per
|
|
5
|
+
process and the SDK keeps working. The check only ever warns: it never raises,
|
|
6
|
+
never delays a call and never changes retry or error behaviour.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import functools
|
|
12
|
+
import re
|
|
13
|
+
import threading
|
|
14
|
+
import warnings
|
|
15
|
+
from typing import Any, Mapping, Optional, Tuple
|
|
16
|
+
|
|
17
|
+
#: Response header (lower-case; header names are case-insensitive).
|
|
18
|
+
MIN_SDK_HEADER = "x-harness-min-sdk"
|
|
19
|
+
|
|
20
|
+
#: Distribution name from ``pyproject.toml``.
|
|
21
|
+
PACKAGE_NAME = "vedika-agent-sdk"
|
|
22
|
+
|
|
23
|
+
# Three dot-separated groups of 1 to 9 ASCII digits. Written as [0-9], not \d,
|
|
24
|
+
# because \d also matches non-ASCII digits in Python.
|
|
25
|
+
_VERSION_CORE = re.compile(r"[0-9]{1,9}\.[0-9]{1,9}\.[0-9]{1,9}")
|
|
26
|
+
|
|
27
|
+
Version = Tuple[int, int, int]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SdkVersionWarning(UserWarning):
|
|
31
|
+
"""This SDK is older than the minimum version the server supports."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_version(value: Any) -> Optional[Version]:
|
|
35
|
+
"""Parse ``major.minor.patch``, ignoring a prerelease or build suffix.
|
|
36
|
+
|
|
37
|
+
Returns ``None`` for anything that is not exactly three dot-separated
|
|
38
|
+
digit groups of at most nine digits each.
|
|
39
|
+
"""
|
|
40
|
+
if not isinstance(value, str):
|
|
41
|
+
return None
|
|
42
|
+
core = re.split(r"[-+]", value.strip(), maxsplit=1)[0]
|
|
43
|
+
if _VERSION_CORE.fullmatch(core) is None:
|
|
44
|
+
return None
|
|
45
|
+
major, minor, patch = core.split(".")
|
|
46
|
+
return int(major), int(minor), int(patch)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def compare_versions(own: Any, minimum: Any) -> Optional[int]:
|
|
50
|
+
"""Compare two versions numerically.
|
|
51
|
+
|
|
52
|
+
Returns -1 when ``own`` is lower than ``minimum``, 0 when equal, 1 when
|
|
53
|
+
higher, and ``None`` when either side cannot be compared.
|
|
54
|
+
"""
|
|
55
|
+
a = parse_version(own)
|
|
56
|
+
b = parse_version(minimum)
|
|
57
|
+
if a is None or b is None:
|
|
58
|
+
return None
|
|
59
|
+
return (a > b) - (a < b)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def warning_message(package_name: str, own_version: str, min_version: str) -> str:
|
|
63
|
+
return (
|
|
64
|
+
f"{package_name} {own_version} is older than the minimum supported version "
|
|
65
|
+
f"{min_version}. Upgrade to {min_version} or later."
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@functools.lru_cache(maxsize=1)
|
|
70
|
+
def _own_version() -> Optional[str]:
|
|
71
|
+
try:
|
|
72
|
+
from importlib.metadata import version
|
|
73
|
+
|
|
74
|
+
return version(PACKAGE_NAME)
|
|
75
|
+
except Exception:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _header_value(headers: Optional[Mapping[str, Any]]) -> Optional[str]:
|
|
80
|
+
if not headers:
|
|
81
|
+
return None
|
|
82
|
+
value = headers.get(MIN_SDK_HEADER)
|
|
83
|
+
if value is None:
|
|
84
|
+
for key, candidate in headers.items():
|
|
85
|
+
if isinstance(key, str) and key.lower() == MIN_SDK_HEADER:
|
|
86
|
+
value = candidate
|
|
87
|
+
break
|
|
88
|
+
if isinstance(value, bytes):
|
|
89
|
+
value = value.decode("ascii", errors="replace")
|
|
90
|
+
return value if isinstance(value, str) else None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
_lock = threading.Lock()
|
|
94
|
+
_warned = False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _reset_min_sdk_warning_for_tests() -> None:
|
|
98
|
+
"""Test-only: allow the once-per-process warning to fire again."""
|
|
99
|
+
global _warned
|
|
100
|
+
with _lock:
|
|
101
|
+
_warned = False
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def check_min_sdk_header(
|
|
105
|
+
headers: Optional[Mapping[str, Any]],
|
|
106
|
+
*,
|
|
107
|
+
own_version: Optional[str] = None,
|
|
108
|
+
stacklevel: int = 4,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Warn once per process when the server's minimum SDK is above ours.
|
|
111
|
+
|
|
112
|
+
``stacklevel`` counts from this function: 2 is the caller, so the default
|
|
113
|
+
of 4 points at the code that called ``agent.threads.<method>()``.
|
|
114
|
+
Swallows every failure, including a warnings filter set to ``error``.
|
|
115
|
+
"""
|
|
116
|
+
global _warned
|
|
117
|
+
if _warned:
|
|
118
|
+
return
|
|
119
|
+
try:
|
|
120
|
+
minimum = parse_version(_header_value(headers))
|
|
121
|
+
if minimum is None:
|
|
122
|
+
return
|
|
123
|
+
own_text = own_version if own_version is not None else _own_version()
|
|
124
|
+
own = parse_version(own_text)
|
|
125
|
+
if own is None or own >= minimum:
|
|
126
|
+
return
|
|
127
|
+
with _lock:
|
|
128
|
+
if _warned:
|
|
129
|
+
return
|
|
130
|
+
_warned = True
|
|
131
|
+
warnings.warn(
|
|
132
|
+
warning_message(PACKAGE_NAME, str(own_text), ".".join(map(str, minimum))),
|
|
133
|
+
SdkVersionWarning,
|
|
134
|
+
stacklevel=stacklevel,
|
|
135
|
+
)
|
|
136
|
+
except Exception:
|
|
137
|
+
# The check must never affect the call that triggered it.
|
|
138
|
+
pass
|
vedika_agent/client.py
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"""HTTP client for the Vedika agent harness.
|
|
2
|
+
|
|
3
|
+
Talks to a harness-server (see vedika-harness/docs/CONTRACT.md) over the
|
|
4
|
+
UI Message Stream v1 wire. The default transport uses ``httpx.AsyncClient``;
|
|
5
|
+
callers can inject any object with a compatible ``stream`` / ``post`` /
|
|
6
|
+
``get`` interface.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import uuid
|
|
12
|
+
from dataclasses import asdict, dataclass, field
|
|
13
|
+
from typing import (
|
|
14
|
+
Any,
|
|
15
|
+
AsyncIterator,
|
|
16
|
+
Awaitable,
|
|
17
|
+
Callable,
|
|
18
|
+
Dict,
|
|
19
|
+
Mapping,
|
|
20
|
+
Optional,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from ._min_sdk import check_min_sdk_header
|
|
24
|
+
from .errors import (
|
|
25
|
+
AuthenticationError,
|
|
26
|
+
InvalidRequestError,
|
|
27
|
+
NotFoundError,
|
|
28
|
+
ProtocolError,
|
|
29
|
+
RateLimitError,
|
|
30
|
+
ServerError,
|
|
31
|
+
VedikaAgentError,
|
|
32
|
+
)
|
|
33
|
+
from .stream import (
|
|
34
|
+
Turn,
|
|
35
|
+
assert_ui_message_stream,
|
|
36
|
+
parse_chunk,
|
|
37
|
+
)
|
|
38
|
+
from .types import (
|
|
39
|
+
CancelTurnOptions,
|
|
40
|
+
CreateThreadOptions,
|
|
41
|
+
MemoryToggles,
|
|
42
|
+
ResumeTurnOptions,
|
|
43
|
+
SendTurnOptions,
|
|
44
|
+
Thread,
|
|
45
|
+
TurnInput,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Header names that callers are not allowed to override.
|
|
49
|
+
RESERVED_HEADERS = frozenset(
|
|
50
|
+
{"authorization", "accept", "content-type", "idempotency-key", "last-event-id",
|
|
51
|
+
"x-vercel-ai-ui-message-stream"}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _uuid4() -> str:
|
|
56
|
+
return str(uuid.uuid4())
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _filter_headers(extra: Optional[Mapping[str, str]]) -> Dict[str, str]:
|
|
60
|
+
out: Dict[str, str] = {}
|
|
61
|
+
if not extra:
|
|
62
|
+
return out
|
|
63
|
+
for key, value in extra.items():
|
|
64
|
+
if key.lower() in RESERVED_HEADERS:
|
|
65
|
+
continue
|
|
66
|
+
out[key] = value
|
|
67
|
+
return out
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class VedikaAgentConfig:
|
|
72
|
+
base_url: str
|
|
73
|
+
api_key: str
|
|
74
|
+
default_headers: Dict[str, str] = field(default_factory=dict)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _HttpResponse:
|
|
78
|
+
__slots__ = ("status", "headers", "body", "_stream_factory")
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
status: int,
|
|
83
|
+
headers: Mapping[str, str],
|
|
84
|
+
body: bytes,
|
|
85
|
+
stream: Optional[Callable[[int], AsyncIterator[bytes]]] = None,
|
|
86
|
+
) -> None:
|
|
87
|
+
self.status = status
|
|
88
|
+
self.headers = dict(headers)
|
|
89
|
+
self.body = body
|
|
90
|
+
self._stream_factory = stream
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def ok(self) -> bool:
|
|
94
|
+
return 200 <= self.status < 300
|
|
95
|
+
|
|
96
|
+
async def text(self) -> str:
|
|
97
|
+
return self.body.decode("utf-8", errors="replace")
|
|
98
|
+
|
|
99
|
+
async def json(self) -> Any:
|
|
100
|
+
import json
|
|
101
|
+
|
|
102
|
+
return json.loads(self.body.decode("utf-8"))
|
|
103
|
+
|
|
104
|
+
def iter_bytes(self) -> AsyncIterator[bytes]:
|
|
105
|
+
if self._stream_factory is None:
|
|
106
|
+
async def _empty() -> AsyncIterator[bytes]:
|
|
107
|
+
if False:
|
|
108
|
+
yield b""
|
|
109
|
+
return _empty()
|
|
110
|
+
return self._stream_factory(self.status)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class _HttpTransport:
|
|
114
|
+
"""Minimal async transport. Default uses ``httpx.AsyncClient``."""
|
|
115
|
+
|
|
116
|
+
def __init__(self) -> None:
|
|
117
|
+
try:
|
|
118
|
+
import httpx # noqa: F401 - presence check
|
|
119
|
+
self._has_httpx = True
|
|
120
|
+
except ImportError: # pragma: no cover
|
|
121
|
+
self._has_httpx = False
|
|
122
|
+
|
|
123
|
+
async def request(
|
|
124
|
+
self,
|
|
125
|
+
method: str,
|
|
126
|
+
url: str,
|
|
127
|
+
*,
|
|
128
|
+
headers: Mapping[str, str],
|
|
129
|
+
body: Optional[bytes] = None,
|
|
130
|
+
stream: bool = False,
|
|
131
|
+
timeout: float = 60.0,
|
|
132
|
+
) -> _HttpResponse:
|
|
133
|
+
if not self._has_httpx: # pragma: no cover
|
|
134
|
+
raise VedikaAgentError(
|
|
135
|
+
"no async HTTP transport available; install httpx or inject a custom transport",
|
|
136
|
+
)
|
|
137
|
+
import httpx
|
|
138
|
+
|
|
139
|
+
if stream:
|
|
140
|
+
# The client and the response must outlive this call: the caller
|
|
141
|
+
# reads the body later, chunk by chunk. Both are closed when the
|
|
142
|
+
# stream is exhausted, abandoned or fails.
|
|
143
|
+
client = httpx.AsyncClient(timeout=timeout)
|
|
144
|
+
try:
|
|
145
|
+
request = client.build_request(method, url, headers=dict(headers), content=body)
|
|
146
|
+
response = await client.send(request, stream=True)
|
|
147
|
+
except BaseException:
|
|
148
|
+
await client.aclose()
|
|
149
|
+
raise
|
|
150
|
+
status = response.status_code
|
|
151
|
+
hdrs = {k: v for k, v in response.headers.items()}
|
|
152
|
+
if not 200 <= status < 300:
|
|
153
|
+
# An error answer is small and the caller maps it from the body.
|
|
154
|
+
try:
|
|
155
|
+
error_body = await response.aread()
|
|
156
|
+
finally:
|
|
157
|
+
await response.aclose()
|
|
158
|
+
await client.aclose()
|
|
159
|
+
return _HttpResponse(status, hdrs, error_body)
|
|
160
|
+
|
|
161
|
+
async def stream_factory(_status: int) -> AsyncIterator[bytes]:
|
|
162
|
+
try:
|
|
163
|
+
async for chunk in response.aiter_bytes():
|
|
164
|
+
if chunk:
|
|
165
|
+
yield chunk
|
|
166
|
+
finally:
|
|
167
|
+
await response.aclose()
|
|
168
|
+
await client.aclose()
|
|
169
|
+
|
|
170
|
+
return _HttpResponse(status, hdrs, b"", stream=stream_factory)
|
|
171
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
172
|
+
request = client.build_request(method, url, headers=dict(headers), content=body)
|
|
173
|
+
response = await client.send(request, stream=False)
|
|
174
|
+
return _HttpResponse(
|
|
175
|
+
response.status_code,
|
|
176
|
+
{k: v for k, v in response.headers.items()},
|
|
177
|
+
response.content,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class Transport:
|
|
182
|
+
"""Protocol-style transport that callers can inject for tests.
|
|
183
|
+
|
|
184
|
+
Implementations must return an object with:
|
|
185
|
+
|
|
186
|
+
- ``status: int``
|
|
187
|
+
- ``headers: Mapping[str, str]``
|
|
188
|
+
- ``body: bytes``
|
|
189
|
+
- ``iter_bytes() -> AsyncIterator[bytes]``
|
|
190
|
+
- ``ok: bool`` (property)
|
|
191
|
+
- ``text() -> Awaitable[str]``
|
|
192
|
+
- ``json() -> Awaitable[Any]``
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
async def request(
|
|
196
|
+
self,
|
|
197
|
+
method: str,
|
|
198
|
+
url: str,
|
|
199
|
+
*,
|
|
200
|
+
headers: Mapping[str, str],
|
|
201
|
+
body: Optional[bytes] = None,
|
|
202
|
+
stream: bool = False,
|
|
203
|
+
) -> _HttpResponse: # pragma: no cover - interface only
|
|
204
|
+
raise NotImplementedError
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _map_response_error(status: int, headers: Mapping[str, str], body: bytes) -> VedikaAgentError:
|
|
208
|
+
import json
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
parsed = json.loads(body.decode("utf-8"))
|
|
212
|
+
except Exception:
|
|
213
|
+
parsed = None
|
|
214
|
+
if isinstance(parsed, dict):
|
|
215
|
+
message = str(parsed.get("message") or parsed.get("error") or "") or None
|
|
216
|
+
code = parsed.get("code")
|
|
217
|
+
else:
|
|
218
|
+
message = None
|
|
219
|
+
code = None
|
|
220
|
+
if message is None:
|
|
221
|
+
message = body.decode("utf-8", errors="replace")[:256] or "request failed"
|
|
222
|
+
if status in (401, 403):
|
|
223
|
+
return AuthenticationError(message, status=status)
|
|
224
|
+
if status in (404, 410):
|
|
225
|
+
return NotFoundError(message)
|
|
226
|
+
if status == 429:
|
|
227
|
+
retry_after = headers.get("retry-after")
|
|
228
|
+
seconds: Optional[int] = None
|
|
229
|
+
if retry_after:
|
|
230
|
+
try:
|
|
231
|
+
seconds = int(retry_after)
|
|
232
|
+
except ValueError:
|
|
233
|
+
seconds = None
|
|
234
|
+
return RateLimitError(message, status=status, retry_after_seconds=seconds)
|
|
235
|
+
if 400 <= status < 500:
|
|
236
|
+
return InvalidRequestError(message, status=status, code=code)
|
|
237
|
+
return ServerError(message, status=status, retryable=status >= 500)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _build_thread(payload: Mapping[str, Any]) -> Thread:
|
|
241
|
+
return Thread(
|
|
242
|
+
id=str(payload["id"]),
|
|
243
|
+
created_at=str(payload.get("createdAt") or payload.get("created_at") or ""),
|
|
244
|
+
metadata=dict(payload.get("metadata") or {}),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class _ThreadsNamespace:
|
|
249
|
+
def __init__(self, parent: "VedikaAgent") -> None:
|
|
250
|
+
self._parent = parent
|
|
251
|
+
|
|
252
|
+
async def create(self, opts: Optional[CreateThreadOptions] = None) -> Thread:
|
|
253
|
+
opts = opts or CreateThreadOptions()
|
|
254
|
+
idem = opts.idempotency_key or _uuid4()
|
|
255
|
+
headers = self._parent._headers(opts.headers)
|
|
256
|
+
headers["idempotency-key"] = idem
|
|
257
|
+
headers["content-type"] = "application/json"
|
|
258
|
+
body_dict: Dict[str, Any] = {"metadata": dict(opts.metadata)}
|
|
259
|
+
import json
|
|
260
|
+
|
|
261
|
+
res = await self._parent._request(
|
|
262
|
+
"POST",
|
|
263
|
+
self._parent._url("/api/v1/agent/threads"),
|
|
264
|
+
headers=headers,
|
|
265
|
+
body=json.dumps(body_dict).encode("utf-8"),
|
|
266
|
+
stream=False,
|
|
267
|
+
)
|
|
268
|
+
if not res.ok:
|
|
269
|
+
raise _map_response_error(res.status, res.headers, res.body)
|
|
270
|
+
try:
|
|
271
|
+
payload = await res.json()
|
|
272
|
+
except Exception as exc:
|
|
273
|
+
raise ProtocolError("response body is not valid JSON") from exc
|
|
274
|
+
return _build_thread(payload)
|
|
275
|
+
|
|
276
|
+
async def send_turn(
|
|
277
|
+
self,
|
|
278
|
+
thread_id: str,
|
|
279
|
+
input: TurnInput,
|
|
280
|
+
opts: Optional[SendTurnOptions] = None,
|
|
281
|
+
) -> Turn:
|
|
282
|
+
opts = opts or SendTurnOptions()
|
|
283
|
+
idem = opts.idempotency_key or _uuid4()
|
|
284
|
+
headers = self._parent._headers(opts.headers)
|
|
285
|
+
headers["idempotency-key"] = idem
|
|
286
|
+
headers["accept"] = "text/event-stream"
|
|
287
|
+
headers["content-type"] = "application/json"
|
|
288
|
+
if opts.model:
|
|
289
|
+
headers["x-vedika-model"] = opts.model
|
|
290
|
+
if opts.memory:
|
|
291
|
+
if not isinstance(opts.memory, MemoryToggles):
|
|
292
|
+
raise VedikaAgentError("memory option must be a MemoryToggles instance")
|
|
293
|
+
headers["x-vedika-memory-old"] = "true" if opts.memory.old_memory else "false"
|
|
294
|
+
headers["x-vedika-memory-sensitive"] = "true" if opts.memory.sensitive_info else "false"
|
|
295
|
+
import json
|
|
296
|
+
|
|
297
|
+
body = json.dumps({"input": input}).encode("utf-8")
|
|
298
|
+
res = await self._parent._request(
|
|
299
|
+
"POST",
|
|
300
|
+
self._parent._url(f"/api/v1/agent/threads/{thread_id}/turns"),
|
|
301
|
+
headers=headers,
|
|
302
|
+
body=body,
|
|
303
|
+
stream=True,
|
|
304
|
+
)
|
|
305
|
+
if not res.ok:
|
|
306
|
+
text = await res.text()
|
|
307
|
+
raise _map_response_error(res.status, res.headers, text.encode("utf-8"))
|
|
308
|
+
assert_ui_message_stream(res.headers)
|
|
309
|
+
turn_id = res.headers.get("x-vedika-turn-id") or "pending"
|
|
310
|
+
return Turn(
|
|
311
|
+
turn_id=turn_id,
|
|
312
|
+
thread_id=thread_id,
|
|
313
|
+
source=res.iter_bytes(),
|
|
314
|
+
cancel=lambda: self.cancel_turn(turn_id),
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
async def resume_turn(self, turn_id: str, opts: ResumeTurnOptions) -> Turn:
|
|
318
|
+
headers = self._parent._headers(opts.headers)
|
|
319
|
+
headers["accept"] = "text/event-stream"
|
|
320
|
+
headers["last-event-id"] = opts.last_event_id
|
|
321
|
+
res = await self._parent._request(
|
|
322
|
+
"GET",
|
|
323
|
+
self._parent._url(f"/api/v1/agent/turns/{turn_id}/events"),
|
|
324
|
+
headers=headers,
|
|
325
|
+
stream=True,
|
|
326
|
+
)
|
|
327
|
+
if not res.ok:
|
|
328
|
+
text = await res.text()
|
|
329
|
+
raise _map_response_error(res.status, res.headers, text.encode("utf-8"))
|
|
330
|
+
assert_ui_message_stream(res.headers)
|
|
331
|
+
thread_id = res.headers.get("x-vedika-thread-id") or "unknown"
|
|
332
|
+
return Turn(
|
|
333
|
+
turn_id=turn_id,
|
|
334
|
+
thread_id=thread_id,
|
|
335
|
+
source=res.iter_bytes(),
|
|
336
|
+
cancel=lambda: self.cancel_turn(turn_id),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
async def cancel_turn(
|
|
340
|
+
self,
|
|
341
|
+
turn_id: str,
|
|
342
|
+
opts: Optional[CancelTurnOptions] = None,
|
|
343
|
+
) -> None:
|
|
344
|
+
opts = opts or CancelTurnOptions()
|
|
345
|
+
headers = self._parent._headers(opts.headers)
|
|
346
|
+
res = await self._parent._request(
|
|
347
|
+
"POST",
|
|
348
|
+
self._parent._url(f"/api/v1/agent/turns/{turn_id}/cancel"),
|
|
349
|
+
headers=headers,
|
|
350
|
+
stream=False,
|
|
351
|
+
)
|
|
352
|
+
if not res.ok and res.status not in (404, 410):
|
|
353
|
+
raise _map_response_error(res.status, res.headers, res.body)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class VedikaAgent:
|
|
357
|
+
"""Top-level client. Construct with {@link create_agent}."""
|
|
358
|
+
|
|
359
|
+
def __init__(
|
|
360
|
+
self,
|
|
361
|
+
config: VedikaAgentConfig,
|
|
362
|
+
*,
|
|
363
|
+
transport: Optional[Transport] = None,
|
|
364
|
+
) -> None:
|
|
365
|
+
self._config = config
|
|
366
|
+
self._transport: Any = transport or _HttpTransport()
|
|
367
|
+
self.threads = _ThreadsNamespace(self)
|
|
368
|
+
|
|
369
|
+
async def _request(self, method: str, url: str, **kwargs: Any) -> Any:
|
|
370
|
+
"""Single choke point: every HTTP response the SDK receives passes here."""
|
|
371
|
+
res = await self._transport.request(method, url, **kwargs)
|
|
372
|
+
check_min_sdk_header(getattr(res, "headers", None))
|
|
373
|
+
return res
|
|
374
|
+
|
|
375
|
+
def _url(self, path: str) -> str:
|
|
376
|
+
from urllib.parse import urljoin
|
|
377
|
+
|
|
378
|
+
return urljoin(self._config.base_url + "/", path.lstrip("/"))
|
|
379
|
+
|
|
380
|
+
def _headers(self, extra: Optional[Mapping[str, str]]) -> Dict[str, str]:
|
|
381
|
+
out: Dict[str, str] = {
|
|
382
|
+
"authorization": f"Bearer {self._config.api_key}",
|
|
383
|
+
**self._config.default_headers,
|
|
384
|
+
}
|
|
385
|
+
out.update(_filter_headers(extra))
|
|
386
|
+
return out
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def create_agent(
|
|
390
|
+
base_url: str,
|
|
391
|
+
api_key: str,
|
|
392
|
+
*,
|
|
393
|
+
default_headers: Optional[Mapping[str, str]] = None,
|
|
394
|
+
transport: Optional[Transport] = None,
|
|
395
|
+
) -> VedikaAgent:
|
|
396
|
+
config = VedikaAgentConfig(
|
|
397
|
+
base_url=base_url,
|
|
398
|
+
api_key=api_key,
|
|
399
|
+
default_headers=dict(default_headers or {}),
|
|
400
|
+
)
|
|
401
|
+
return VedikaAgent(config, transport=transport)
|