guidinghand 0.1.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.
- guidinghand/__init__.py +117 -0
- guidinghand/_async_client.py +428 -0
- guidinghand/_base.py +207 -0
- guidinghand/_client.py +501 -0
- guidinghand/_errors.py +213 -0
- guidinghand/_types.py +288 -0
- guidinghand/_version.py +1 -0
- guidinghand/_webhooks.py +79 -0
- guidinghand/py.typed +0 -0
- guidinghand-0.1.0.dist-info/METADATA +352 -0
- guidinghand-0.1.0.dist-info/RECORD +13 -0
- guidinghand-0.1.0.dist-info/WHEEL +4 -0
- guidinghand-0.1.0.dist-info/licenses/LICENSE +21 -0
guidinghand/__init__.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""The official Python SDK for the GuidingHand API.
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
from guidinghand import GuidingHand
|
|
5
|
+
|
|
6
|
+
client = GuidingHand() # reads GUIDINGHAND_API_KEY
|
|
7
|
+
session = client.sessions.create()
|
|
8
|
+
print("Send this link:", session["invite_url"])
|
|
9
|
+
client.sessions.wait_for_connection(session["session_id"])
|
|
10
|
+
task = client.tasks.run(
|
|
11
|
+
session["session_id"],
|
|
12
|
+
"Turn on Dark Mode",
|
|
13
|
+
on_question=lambda q: input(q["question"] + " > "),
|
|
14
|
+
on_approval=lambda a: input(f"Approve {a['action']}? [y/N] ") == "y",
|
|
15
|
+
)
|
|
16
|
+
print(task["status"], task["result"])
|
|
17
|
+
```
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from ._async_client import AsyncGuidingHand
|
|
21
|
+
from ._base import DEFAULT_BASE_URL
|
|
22
|
+
from ._client import GuidingHand
|
|
23
|
+
from ._errors import (
|
|
24
|
+
APIConnectionError,
|
|
25
|
+
APIError,
|
|
26
|
+
AuthenticationError,
|
|
27
|
+
ConflictError,
|
|
28
|
+
GuidingHandError,
|
|
29
|
+
GuidingHandTimeout,
|
|
30
|
+
InvalidRequestError,
|
|
31
|
+
NeedsInput,
|
|
32
|
+
NotFoundError,
|
|
33
|
+
PaymentRequiredError,
|
|
34
|
+
PermissionDeniedError,
|
|
35
|
+
RateLimitError,
|
|
36
|
+
SessionExpired,
|
|
37
|
+
WebhookVerificationError,
|
|
38
|
+
)
|
|
39
|
+
from ._types import (
|
|
40
|
+
Agent,
|
|
41
|
+
AgentPage,
|
|
42
|
+
ApprovalDecision,
|
|
43
|
+
ApprovalPending,
|
|
44
|
+
Deleted,
|
|
45
|
+
Device,
|
|
46
|
+
Effort,
|
|
47
|
+
Event,
|
|
48
|
+
EventPage,
|
|
49
|
+
EventType,
|
|
50
|
+
Metadata,
|
|
51
|
+
Page,
|
|
52
|
+
Pending,
|
|
53
|
+
QuestionPending,
|
|
54
|
+
Recording,
|
|
55
|
+
RecordingFrame,
|
|
56
|
+
Session,
|
|
57
|
+
SessionPage,
|
|
58
|
+
SessionStatus,
|
|
59
|
+
Task,
|
|
60
|
+
TaskPage,
|
|
61
|
+
TaskStatus,
|
|
62
|
+
Webhook,
|
|
63
|
+
WebhookEvent,
|
|
64
|
+
WebhookEventType,
|
|
65
|
+
)
|
|
66
|
+
from ._version import __version__
|
|
67
|
+
from ._webhooks import SIGNATURE_HEADER, verify_webhook
|
|
68
|
+
|
|
69
|
+
__all__ = [
|
|
70
|
+
"__version__",
|
|
71
|
+
"GuidingHand",
|
|
72
|
+
"AsyncGuidingHand",
|
|
73
|
+
"DEFAULT_BASE_URL",
|
|
74
|
+
"verify_webhook",
|
|
75
|
+
"SIGNATURE_HEADER",
|
|
76
|
+
# errors
|
|
77
|
+
"GuidingHandError",
|
|
78
|
+
"InvalidRequestError",
|
|
79
|
+
"AuthenticationError",
|
|
80
|
+
"PaymentRequiredError",
|
|
81
|
+
"PermissionDeniedError",
|
|
82
|
+
"NotFoundError",
|
|
83
|
+
"ConflictError",
|
|
84
|
+
"RateLimitError",
|
|
85
|
+
"APIError",
|
|
86
|
+
"APIConnectionError",
|
|
87
|
+
"GuidingHandTimeout",
|
|
88
|
+
"NeedsInput",
|
|
89
|
+
"SessionExpired",
|
|
90
|
+
"WebhookVerificationError",
|
|
91
|
+
# objects
|
|
92
|
+
"Agent",
|
|
93
|
+
"AgentPage",
|
|
94
|
+
"ApprovalDecision",
|
|
95
|
+
"ApprovalPending",
|
|
96
|
+
"Deleted",
|
|
97
|
+
"Device",
|
|
98
|
+
"Effort",
|
|
99
|
+
"Event",
|
|
100
|
+
"EventPage",
|
|
101
|
+
"EventType",
|
|
102
|
+
"Metadata",
|
|
103
|
+
"Page",
|
|
104
|
+
"Pending",
|
|
105
|
+
"QuestionPending",
|
|
106
|
+
"Recording",
|
|
107
|
+
"RecordingFrame",
|
|
108
|
+
"Session",
|
|
109
|
+
"SessionPage",
|
|
110
|
+
"SessionStatus",
|
|
111
|
+
"Task",
|
|
112
|
+
"TaskPage",
|
|
113
|
+
"TaskStatus",
|
|
114
|
+
"Webhook",
|
|
115
|
+
"WebhookEvent",
|
|
116
|
+
"WebhookEventType",
|
|
117
|
+
]
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""The asyncio client: ``AsyncGuidingHand``. Same surface as ``GuidingHand``, with ``await``."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, AsyncIterator, Awaitable, Callable, Dict, List, Optional, Set, TypeVar, Union, cast
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from ._base import (
|
|
11
|
+
DEFAULT_BASE_URL,
|
|
12
|
+
DEFAULT_MAX_RETRIES,
|
|
13
|
+
DEFAULT_TIMEOUT,
|
|
14
|
+
LONG_POLL_MS,
|
|
15
|
+
Include,
|
|
16
|
+
answer_of,
|
|
17
|
+
api_path,
|
|
18
|
+
approval_reply,
|
|
19
|
+
compact,
|
|
20
|
+
connection_error,
|
|
21
|
+
decision_of,
|
|
22
|
+
default_headers,
|
|
23
|
+
deleted_before,
|
|
24
|
+
events_timeout,
|
|
25
|
+
include_param,
|
|
26
|
+
is_retryable,
|
|
27
|
+
is_retryable_status,
|
|
28
|
+
may_have_happened,
|
|
29
|
+
parse_response,
|
|
30
|
+
pending_key,
|
|
31
|
+
resolve_api_key,
|
|
32
|
+
retry_delay,
|
|
33
|
+
run_wait_ms,
|
|
34
|
+
webhook_body,
|
|
35
|
+
)
|
|
36
|
+
from ._errors import ConflictError, GuidingHandError, GuidingHandTimeout, NeedsInput, SessionExpired
|
|
37
|
+
from ._types import (
|
|
38
|
+
Agent,
|
|
39
|
+
AgentPage,
|
|
40
|
+
ApprovalDecision,
|
|
41
|
+
ApprovalPending,
|
|
42
|
+
Deleted,
|
|
43
|
+
Effort,
|
|
44
|
+
Event,
|
|
45
|
+
EventPage,
|
|
46
|
+
Metadata,
|
|
47
|
+
Pending,
|
|
48
|
+
QuestionPending,
|
|
49
|
+
Recording,
|
|
50
|
+
Session,
|
|
51
|
+
SessionPage,
|
|
52
|
+
Task,
|
|
53
|
+
TaskPage,
|
|
54
|
+
TaskStatus,
|
|
55
|
+
Webhook,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
__all__ = ["AsyncGuidingHand"]
|
|
59
|
+
|
|
60
|
+
T = TypeVar("T")
|
|
61
|
+
MaybeAwaitable = Union[T, Awaitable[T]]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def _resolve(value: MaybeAwaitable[T]) -> T:
|
|
65
|
+
if inspect.isawaitable(value):
|
|
66
|
+
return await value
|
|
67
|
+
return value
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class AsyncGuidingHand:
|
|
71
|
+
"""An asyncio client for the GuidingHand API, with the same resources and methods as ``GuidingHand``.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
async with AsyncGuidingHand() as client:
|
|
75
|
+
session = await client.sessions.create()
|
|
76
|
+
```
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
api_key: Optional[str] = None,
|
|
82
|
+
*,
|
|
83
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
84
|
+
timeout: Optional[float] = DEFAULT_TIMEOUT,
|
|
85
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
86
|
+
) -> None:
|
|
87
|
+
self.api_key = resolve_api_key(api_key)
|
|
88
|
+
self.base_url = base_url.rstrip("/")
|
|
89
|
+
self.timeout = timeout
|
|
90
|
+
self.max_retries = max(0, int(max_retries))
|
|
91
|
+
self._http = httpx.AsyncClient(base_url=self.base_url, headers=default_headers(self.api_key), timeout=timeout)
|
|
92
|
+
self.agents = AsyncAgents(self)
|
|
93
|
+
self.sessions = AsyncSessions(self)
|
|
94
|
+
self.tasks = AsyncTasks(self)
|
|
95
|
+
self.webhook = AsyncWebhookEndpoint(self)
|
|
96
|
+
|
|
97
|
+
async def _request(
|
|
98
|
+
self,
|
|
99
|
+
method: str,
|
|
100
|
+
path: str,
|
|
101
|
+
*,
|
|
102
|
+
params: Optional[Dict[str, Any]] = None,
|
|
103
|
+
body: Any = None,
|
|
104
|
+
timeout: Optional[float] = None,
|
|
105
|
+
raw: bool = False,
|
|
106
|
+
deleted_result: Optional[Dict[str, Any]] = None,
|
|
107
|
+
) -> Any:
|
|
108
|
+
"""``deleted_result``: for a DELETE, what to return when a retry gets 404 because an earlier attempt
|
|
109
|
+
(whose response was lost) already deleted it."""
|
|
110
|
+
retryable = is_retryable(method, body)
|
|
111
|
+
attempt = 0
|
|
112
|
+
earlier_may_have_happened = False
|
|
113
|
+
while True:
|
|
114
|
+
try:
|
|
115
|
+
response = await self._http.request(
|
|
116
|
+
method,
|
|
117
|
+
path,
|
|
118
|
+
params=compact(params) or None,
|
|
119
|
+
json=body,
|
|
120
|
+
timeout=timeout if timeout is not None else self.timeout,
|
|
121
|
+
)
|
|
122
|
+
except httpx.TransportError as e:
|
|
123
|
+
if retryable and attempt < self.max_retries:
|
|
124
|
+
earlier_may_have_happened = True
|
|
125
|
+
await asyncio.sleep(retry_delay(attempt))
|
|
126
|
+
attempt += 1
|
|
127
|
+
continue
|
|
128
|
+
raise connection_error(method, path, e) from e
|
|
129
|
+
if retryable and is_retryable_status(response.status_code) and attempt < self.max_retries:
|
|
130
|
+
earlier_may_have_happened = earlier_may_have_happened or may_have_happened(response)
|
|
131
|
+
delay = retry_delay(attempt, response)
|
|
132
|
+
await response.aclose()
|
|
133
|
+
await asyncio.sleep(delay)
|
|
134
|
+
attempt += 1
|
|
135
|
+
continue
|
|
136
|
+
if deleted_result is not None and deleted_before(method, response, earlier_may_have_happened):
|
|
137
|
+
return deleted_result
|
|
138
|
+
return parse_response(method, path, response, raw)
|
|
139
|
+
|
|
140
|
+
async def close(self) -> None:
|
|
141
|
+
"""Closes the HTTP connections. The client can't be used afterwards."""
|
|
142
|
+
await self._http.aclose()
|
|
143
|
+
|
|
144
|
+
aclose = close
|
|
145
|
+
|
|
146
|
+
async def __aenter__(self) -> "AsyncGuidingHand":
|
|
147
|
+
return self
|
|
148
|
+
|
|
149
|
+
async def __aexit__(self, *exc: Any) -> None:
|
|
150
|
+
await self.close()
|
|
151
|
+
|
|
152
|
+
def __repr__(self) -> str:
|
|
153
|
+
return f"AsyncGuidingHand(base_url={self.base_url!r})"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class AsyncAgents:
|
|
157
|
+
"""``client.agents``: see ``guidinghand.GuidingHand.agents``."""
|
|
158
|
+
|
|
159
|
+
def __init__(self, client: AsyncGuidingHand) -> None:
|
|
160
|
+
self._client = client
|
|
161
|
+
|
|
162
|
+
async def list(self) -> AgentPage:
|
|
163
|
+
return cast(AgentPage, await self._client._request("GET", api_path("agents")))
|
|
164
|
+
|
|
165
|
+
async def create(
|
|
166
|
+
self,
|
|
167
|
+
name: str,
|
|
168
|
+
agent_id: Optional[str] = None,
|
|
169
|
+
instructions: Optional[str] = None,
|
|
170
|
+
effort: Optional[Effort] = None,
|
|
171
|
+
greeting: Optional[str] = None,
|
|
172
|
+
) -> Agent:
|
|
173
|
+
body = compact(
|
|
174
|
+
{"name": name, "agent_id": agent_id, "instructions": instructions, "effort": effort, "greeting": greeting}
|
|
175
|
+
)
|
|
176
|
+
return cast(Agent, await self._client._request("POST", api_path("agents"), body=body))
|
|
177
|
+
|
|
178
|
+
async def retrieve(self, agent_id: str) -> Agent:
|
|
179
|
+
return cast(Agent, await self._client._request("GET", api_path("agents", agent_id)))
|
|
180
|
+
|
|
181
|
+
async def update(self, agent_id: str, **fields: Any) -> Agent:
|
|
182
|
+
return cast(Agent, await self._client._request("PATCH", api_path("agents", agent_id), body=fields))
|
|
183
|
+
|
|
184
|
+
async def delete(self, agent_id: str) -> Deleted:
|
|
185
|
+
gone = {"object": "agent", "agent_id": agent_id, "deleted": True}
|
|
186
|
+
return cast(Deleted, await self._client._request("DELETE", api_path("agents", agent_id), deleted_result=gone))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class AsyncSessions:
|
|
190
|
+
"""``client.sessions``: see ``guidinghand.GuidingHand.sessions``."""
|
|
191
|
+
|
|
192
|
+
def __init__(self, client: AsyncGuidingHand) -> None:
|
|
193
|
+
self._client = client
|
|
194
|
+
|
|
195
|
+
async def create(self, agent_id: Optional[str] = None, metadata: Optional[Metadata] = None) -> Session:
|
|
196
|
+
body = compact({"agent_id": agent_id, "metadata": metadata})
|
|
197
|
+
return cast(Session, await self._client._request("POST", api_path("sessions"), body=body))
|
|
198
|
+
|
|
199
|
+
async def list(
|
|
200
|
+
self, agent_id: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[str] = None
|
|
201
|
+
) -> SessionPage:
|
|
202
|
+
params = {"agent_id": agent_id, "limit": limit, "cursor": cursor}
|
|
203
|
+
return cast(SessionPage, await self._client._request("GET", api_path("sessions"), params=params))
|
|
204
|
+
|
|
205
|
+
async def list_all(self, agent_id: Optional[str] = None, *, limit: Optional[int] = None) -> AsyncIterator[Session]:
|
|
206
|
+
cursor: Optional[str] = None
|
|
207
|
+
while True:
|
|
208
|
+
page = await self.list(agent_id=agent_id, limit=limit, cursor=cursor)
|
|
209
|
+
for session in page["data"]:
|
|
210
|
+
yield session
|
|
211
|
+
if not page["has_more"] or not page["next_cursor"]:
|
|
212
|
+
return
|
|
213
|
+
cursor = page["next_cursor"]
|
|
214
|
+
|
|
215
|
+
async def retrieve(self, session_id: str) -> Session:
|
|
216
|
+
return cast(Session, await self._client._request("GET", api_path("sessions", session_id)))
|
|
217
|
+
|
|
218
|
+
async def delete(self, session_id: str) -> Deleted:
|
|
219
|
+
gone = {"object": "session", "session_id": session_id, "deleted": True}
|
|
220
|
+
return cast(
|
|
221
|
+
Deleted, await self._client._request("DELETE", api_path("sessions", session_id), deleted_result=gone)
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
async def wait_for_connection(
|
|
225
|
+
self, session_id: str, timeout: Optional[float] = 600, poll_interval: float = 3
|
|
226
|
+
) -> Session:
|
|
227
|
+
deadline = None if timeout is None else time.monotonic() + timeout
|
|
228
|
+
while True:
|
|
229
|
+
session = await self.retrieve(session_id)
|
|
230
|
+
if session["status"] == "connected":
|
|
231
|
+
return session
|
|
232
|
+
if session["status"] == "expired":
|
|
233
|
+
raise SessionExpired(session)
|
|
234
|
+
remaining = None if deadline is None else deadline - time.monotonic()
|
|
235
|
+
if remaining is not None and remaining <= 0:
|
|
236
|
+
raise GuidingHandTimeout(
|
|
237
|
+
f"No computer connected to session {session_id} within {timeout:g} s.", session=session
|
|
238
|
+
)
|
|
239
|
+
await asyncio.sleep(poll_interval if remaining is None else max(0.0, min(poll_interval, remaining)))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class AsyncTasks:
|
|
243
|
+
"""``client.tasks``: see ``guidinghand.GuidingHand.tasks``."""
|
|
244
|
+
|
|
245
|
+
def __init__(self, client: AsyncGuidingHand) -> None:
|
|
246
|
+
self._client = client
|
|
247
|
+
|
|
248
|
+
async def create(
|
|
249
|
+
self,
|
|
250
|
+
session_id: str,
|
|
251
|
+
prompt: str,
|
|
252
|
+
agent_id: Optional[str] = None,
|
|
253
|
+
request_id: Optional[str] = None,
|
|
254
|
+
metadata: Optional[Metadata] = None,
|
|
255
|
+
) -> Task:
|
|
256
|
+
body = compact({"prompt": prompt, "agent_id": agent_id, "request_id": request_id, "metadata": metadata})
|
|
257
|
+
return cast(Task, await self._client._request("POST", api_path("sessions", session_id, "tasks"), body=body))
|
|
258
|
+
|
|
259
|
+
async def list(
|
|
260
|
+
self,
|
|
261
|
+
session_id: Optional[str] = None,
|
|
262
|
+
agent_id: Optional[str] = None,
|
|
263
|
+
status: Optional[TaskStatus] = None,
|
|
264
|
+
limit: Optional[int] = None,
|
|
265
|
+
cursor: Optional[str] = None,
|
|
266
|
+
) -> TaskPage:
|
|
267
|
+
params = {"session_id": session_id, "agent_id": agent_id, "status": status, "limit": limit, "cursor": cursor}
|
|
268
|
+
return cast(TaskPage, await self._client._request("GET", api_path("tasks"), params=params))
|
|
269
|
+
|
|
270
|
+
async def list_all(
|
|
271
|
+
self,
|
|
272
|
+
session_id: Optional[str] = None,
|
|
273
|
+
agent_id: Optional[str] = None,
|
|
274
|
+
status: Optional[TaskStatus] = None,
|
|
275
|
+
*,
|
|
276
|
+
limit: Optional[int] = None,
|
|
277
|
+
) -> AsyncIterator[Task]:
|
|
278
|
+
cursor: Optional[str] = None
|
|
279
|
+
while True:
|
|
280
|
+
page = await self.list(session_id=session_id, agent_id=agent_id, status=status, limit=limit, cursor=cursor)
|
|
281
|
+
for task in page["data"]:
|
|
282
|
+
yield task
|
|
283
|
+
if not page["has_more"] or not page["next_cursor"]:
|
|
284
|
+
return
|
|
285
|
+
cursor = page["next_cursor"]
|
|
286
|
+
|
|
287
|
+
async def retrieve(self, task_id: str, include: Include = None) -> Task:
|
|
288
|
+
params = {"include": include_param(include)}
|
|
289
|
+
return cast(Task, await self._client._request("GET", api_path("tasks", task_id), params=params))
|
|
290
|
+
|
|
291
|
+
async def events(self, task_id: str, after: int = 0, wait_ms: int = 0) -> EventPage:
|
|
292
|
+
params = {"after": after, "wait_ms": wait_ms}
|
|
293
|
+
timeout = events_timeout(self._client.timeout, wait_ms)
|
|
294
|
+
return cast(
|
|
295
|
+
EventPage,
|
|
296
|
+
await self._client._request("GET", api_path("tasks", task_id, "events"), params=params, timeout=timeout),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
async def respond(
|
|
300
|
+
self,
|
|
301
|
+
task_id: str,
|
|
302
|
+
question_id: Optional[str] = None,
|
|
303
|
+
answer: Optional[str] = None,
|
|
304
|
+
approval_id: Optional[str] = None,
|
|
305
|
+
decision: Optional[Union[str, bool]] = None,
|
|
306
|
+
note: Optional[str] = None,
|
|
307
|
+
) -> Task:
|
|
308
|
+
body = compact(
|
|
309
|
+
{
|
|
310
|
+
"question_id": question_id,
|
|
311
|
+
"answer": answer,
|
|
312
|
+
"approval_id": approval_id,
|
|
313
|
+
"decision": decision_of(decision) if isinstance(decision, bool) else decision,
|
|
314
|
+
"note": note,
|
|
315
|
+
}
|
|
316
|
+
)
|
|
317
|
+
return cast(Task, await self._client._request("POST", api_path("tasks", task_id, "respond"), body=body))
|
|
318
|
+
|
|
319
|
+
async def stop(self, task_id: str) -> Task:
|
|
320
|
+
return cast(Task, await self._client._request("POST", api_path("tasks", task_id, "stop"), body={}))
|
|
321
|
+
|
|
322
|
+
async def recording(self, task_id: str) -> Recording:
|
|
323
|
+
return cast(Recording, await self._client._request("GET", api_path("tasks", task_id, "recording")))
|
|
324
|
+
|
|
325
|
+
async def recording_frame(self, task_id: str, seq: int) -> bytes:
|
|
326
|
+
return cast(bytes, await self._client._request("GET", api_path("tasks", task_id, "recording", seq), raw=True))
|
|
327
|
+
|
|
328
|
+
async def stream(self, task_id: str, after: int = 0) -> AsyncIterator[Event]:
|
|
329
|
+
"""Yields the task's events as they happen, and ends when the task is done (``async for``)."""
|
|
330
|
+
while True:
|
|
331
|
+
page = await self.events(task_id, after=after, wait_ms=LONG_POLL_MS)
|
|
332
|
+
for event in page["data"]:
|
|
333
|
+
yield event
|
|
334
|
+
after = page["cursor"]
|
|
335
|
+
if page["task"]["done"]:
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
async def run(
|
|
339
|
+
self,
|
|
340
|
+
session_id: str,
|
|
341
|
+
prompt: str,
|
|
342
|
+
*,
|
|
343
|
+
agent_id: Optional[str] = None,
|
|
344
|
+
on_event: Optional[Callable[[Event], MaybeAwaitable[Any]]] = None,
|
|
345
|
+
on_question: Optional[Callable[[QuestionPending], MaybeAwaitable[str]]] = None,
|
|
346
|
+
on_approval: Optional[Callable[[ApprovalPending], MaybeAwaitable[Union[bool, str, ApprovalDecision]]]] = None,
|
|
347
|
+
metadata: Optional[Metadata] = None,
|
|
348
|
+
request_id: Optional[str] = None,
|
|
349
|
+
timeout: Optional[float] = None,
|
|
350
|
+
) -> Task:
|
|
351
|
+
"""Starts a task and drives it to the end: see ``GuidingHand.tasks.run``. Handlers may be async."""
|
|
352
|
+
deadline = None if timeout is None else time.monotonic() + timeout
|
|
353
|
+
task = await self.create(session_id, prompt, agent_id=agent_id, request_id=request_id, metadata=metadata)
|
|
354
|
+
task_id = task["task_id"]
|
|
355
|
+
after = 0
|
|
356
|
+
answered: Set[str] = set()
|
|
357
|
+
while True:
|
|
358
|
+
wait_ms = run_wait_ms(deadline)
|
|
359
|
+
if wait_ms is None:
|
|
360
|
+
try:
|
|
361
|
+
task = await self.stop(task_id)
|
|
362
|
+
except GuidingHandError as e:
|
|
363
|
+
raise GuidingHandTimeout(
|
|
364
|
+
f"Task {task_id} wasn't done within {timeout:g} s, and stopping it failed: "
|
|
365
|
+
"it may still be running.",
|
|
366
|
+
task=task,
|
|
367
|
+
) from e
|
|
368
|
+
if task["done"] and not task.get("interrupted"):
|
|
369
|
+
# It finished before the stop reached it: pass on the events not seen yet, and return it.
|
|
370
|
+
if on_event is not None:
|
|
371
|
+
for event in (await self.events(task_id, after=after))["data"]:
|
|
372
|
+
await _resolve(on_event(event))
|
|
373
|
+
return task
|
|
374
|
+
raise GuidingHandTimeout(f"Task {task_id} wasn't done within {timeout:g} s, so it was stopped.", task=task)
|
|
375
|
+
page = await self.events(task_id, after=after, wait_ms=wait_ms)
|
|
376
|
+
for event in page["data"]:
|
|
377
|
+
if on_event is not None:
|
|
378
|
+
await _resolve(on_event(event))
|
|
379
|
+
after = page["cursor"]
|
|
380
|
+
task = page["task"]
|
|
381
|
+
if task["done"]:
|
|
382
|
+
return task
|
|
383
|
+
pending = task.get("pending")
|
|
384
|
+
if pending and pending_key(pending) not in answered:
|
|
385
|
+
await self._answer(task, pending, on_question, on_approval)
|
|
386
|
+
answered.add(pending_key(pending))
|
|
387
|
+
|
|
388
|
+
async def _answer(
|
|
389
|
+
self,
|
|
390
|
+
task: Task,
|
|
391
|
+
pending: Pending,
|
|
392
|
+
on_question: Optional[Callable[[QuestionPending], MaybeAwaitable[str]]],
|
|
393
|
+
on_approval: Optional[Callable[[ApprovalPending], MaybeAwaitable[Union[bool, str, ApprovalDecision]]]],
|
|
394
|
+
) -> None:
|
|
395
|
+
reply: Dict[str, Any]
|
|
396
|
+
if pending["type"] == "question":
|
|
397
|
+
if on_question is None:
|
|
398
|
+
raise NeedsInput(task)
|
|
399
|
+
reply = {"question_id": pending["question_id"], "answer": answer_of(await _resolve(on_question(pending)))}
|
|
400
|
+
else:
|
|
401
|
+
if on_approval is None:
|
|
402
|
+
raise NeedsInput(task)
|
|
403
|
+
reply = {"approval_id": pending["approval_id"], **approval_reply(await _resolve(on_approval(pending)))}
|
|
404
|
+
try:
|
|
405
|
+
await self.respond(task["task_id"], **reply)
|
|
406
|
+
except ConflictError:
|
|
407
|
+
pass # no longer pending (answered elsewhere, or the task ended): the next events call shows where it is
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
class AsyncWebhookEndpoint:
|
|
411
|
+
"""``client.webhook``: see ``guidinghand.GuidingHand.webhook``."""
|
|
412
|
+
|
|
413
|
+
def __init__(self, client: AsyncGuidingHand) -> None:
|
|
414
|
+
self._client = client
|
|
415
|
+
|
|
416
|
+
async def retrieve(self) -> Webhook:
|
|
417
|
+
return cast(Webhook, await self._client._request("GET", "/v1/webhook"))
|
|
418
|
+
|
|
419
|
+
async def update(
|
|
420
|
+
self, url: Optional[str] = None, events: Optional[List[str]] = None, rotate_secret: bool = False
|
|
421
|
+
) -> Webhook:
|
|
422
|
+
"""Changes only what you pass (leave ``url`` or ``events`` out to keep them): see
|
|
423
|
+
``GuidingHand.webhook.update``. To remove the endpoint, use ``delete()``."""
|
|
424
|
+
body = webhook_body(url, events, rotate_secret)
|
|
425
|
+
return cast(Webhook, await self._client._request("PUT", "/v1/webhook", body=body))
|
|
426
|
+
|
|
427
|
+
async def delete(self) -> Webhook:
|
|
428
|
+
return cast(Webhook, await self._client._request("PUT", "/v1/webhook", body={"url": None}))
|