oagi-core 0.10.1__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.
- oagi/__init__.py +148 -0
- oagi/agent/__init__.py +33 -0
- oagi/agent/default.py +124 -0
- oagi/agent/factories.py +74 -0
- oagi/agent/observer/__init__.py +38 -0
- oagi/agent/observer/agent_observer.py +99 -0
- oagi/agent/observer/events.py +28 -0
- oagi/agent/observer/exporters.py +445 -0
- oagi/agent/observer/protocol.py +12 -0
- oagi/agent/protocol.py +55 -0
- oagi/agent/registry.py +155 -0
- oagi/agent/tasker/__init__.py +33 -0
- oagi/agent/tasker/memory.py +160 -0
- oagi/agent/tasker/models.py +77 -0
- oagi/agent/tasker/planner.py +408 -0
- oagi/agent/tasker/taskee_agent.py +512 -0
- oagi/agent/tasker/tasker_agent.py +324 -0
- oagi/cli/__init__.py +11 -0
- oagi/cli/agent.py +281 -0
- oagi/cli/display.py +56 -0
- oagi/cli/main.py +77 -0
- oagi/cli/server.py +94 -0
- oagi/cli/tracking.py +55 -0
- oagi/cli/utils.py +89 -0
- oagi/client/__init__.py +12 -0
- oagi/client/async_.py +290 -0
- oagi/client/base.py +457 -0
- oagi/client/sync.py +293 -0
- oagi/exceptions.py +118 -0
- oagi/handler/__init__.py +24 -0
- oagi/handler/_macos.py +55 -0
- oagi/handler/async_pyautogui_action_handler.py +44 -0
- oagi/handler/async_screenshot_maker.py +47 -0
- oagi/handler/pil_image.py +102 -0
- oagi/handler/pyautogui_action_handler.py +291 -0
- oagi/handler/screenshot_maker.py +41 -0
- oagi/logging.py +55 -0
- oagi/server/__init__.py +13 -0
- oagi/server/agent_wrappers.py +98 -0
- oagi/server/config.py +46 -0
- oagi/server/main.py +157 -0
- oagi/server/models.py +98 -0
- oagi/server/session_store.py +116 -0
- oagi/server/socketio_server.py +405 -0
- oagi/task/__init__.py +21 -0
- oagi/task/async_.py +101 -0
- oagi/task/async_short.py +76 -0
- oagi/task/base.py +157 -0
- oagi/task/short.py +76 -0
- oagi/task/sync.py +99 -0
- oagi/types/__init__.py +50 -0
- oagi/types/action_handler.py +30 -0
- oagi/types/async_action_handler.py +30 -0
- oagi/types/async_image_provider.py +38 -0
- oagi/types/image.py +17 -0
- oagi/types/image_provider.py +35 -0
- oagi/types/models/__init__.py +32 -0
- oagi/types/models/action.py +33 -0
- oagi/types/models/client.py +68 -0
- oagi/types/models/image_config.py +47 -0
- oagi/types/models/step.py +17 -0
- oagi/types/step_observer.py +93 -0
- oagi/types/url.py +3 -0
- oagi_core-0.10.1.dist-info/METADATA +245 -0
- oagi_core-0.10.1.dist-info/RECORD +68 -0
- oagi_core-0.10.1.dist-info/WHEEL +4 -0
- oagi_core-0.10.1.dist-info/entry_points.txt +2 -0
- oagi_core-0.10.1.dist-info/licenses/LICENSE +21 -0
oagi/client/base.py
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) OpenAGI Foundation
|
|
3
|
+
# All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# This file is part of the official API project.
|
|
6
|
+
# Licensed under the MIT License.
|
|
7
|
+
# -----------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any, Generic, TypeVar
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from ..exceptions import (
|
|
15
|
+
APIError,
|
|
16
|
+
AuthenticationError,
|
|
17
|
+
ConfigurationError,
|
|
18
|
+
NetworkError,
|
|
19
|
+
NotFoundError,
|
|
20
|
+
RateLimitError,
|
|
21
|
+
RequestTimeoutError,
|
|
22
|
+
ServerError,
|
|
23
|
+
ValidationError,
|
|
24
|
+
)
|
|
25
|
+
from ..logging import get_logger
|
|
26
|
+
from ..types.models import (
|
|
27
|
+
ErrorResponse,
|
|
28
|
+
GenerateResponse,
|
|
29
|
+
LLMResponse,
|
|
30
|
+
UploadFileResponse,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
logger = get_logger("client.base")
|
|
34
|
+
|
|
35
|
+
# TypeVar for HTTP client type (httpx.Client or httpx.AsyncClient)
|
|
36
|
+
HttpClientT = TypeVar("HttpClientT")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class BaseClient(Generic[HttpClientT]):
|
|
40
|
+
"""Base class with shared business logic for sync/async clients."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, base_url: str | None = None, api_key: str | None = None):
|
|
43
|
+
# Get from environment if not provided
|
|
44
|
+
self.base_url = (
|
|
45
|
+
base_url or os.getenv("OAGI_BASE_URL") or "https://api.agiopen.org"
|
|
46
|
+
)
|
|
47
|
+
self.api_key = api_key or os.getenv("OAGI_API_KEY")
|
|
48
|
+
|
|
49
|
+
# Validate required configuration
|
|
50
|
+
if not self.api_key:
|
|
51
|
+
raise ConfigurationError(
|
|
52
|
+
"OAGI API key must be provided either as 'api_key' parameter or "
|
|
53
|
+
"OAGI_API_KEY environment variable"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
self.base_url = self.base_url.rstrip("/")
|
|
57
|
+
self.timeout = 60
|
|
58
|
+
self.client: HttpClientT # Will be set by subclasses
|
|
59
|
+
|
|
60
|
+
logger.info(f"Client initialized with base_url: {self.base_url}")
|
|
61
|
+
|
|
62
|
+
def _build_headers(self, api_version: str | None = None) -> dict[str, str]:
|
|
63
|
+
headers: dict[str, str] = {}
|
|
64
|
+
if api_version:
|
|
65
|
+
headers["x-api-version"] = api_version
|
|
66
|
+
if self.api_key:
|
|
67
|
+
headers["x-api-key"] = self.api_key
|
|
68
|
+
return headers
|
|
69
|
+
|
|
70
|
+
def _build_payload(
|
|
71
|
+
self,
|
|
72
|
+
model: str,
|
|
73
|
+
messages_history: list,
|
|
74
|
+
task_description: str | None = None,
|
|
75
|
+
task_id: str | None = None,
|
|
76
|
+
temperature: float | None = None,
|
|
77
|
+
) -> dict[str, Any]:
|
|
78
|
+
"""Build OpenAI-compatible request payload.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
model: Model to use
|
|
82
|
+
messages_history: OpenAI-compatible message history
|
|
83
|
+
task_description: Task description
|
|
84
|
+
task_id: Task ID for continuing session
|
|
85
|
+
temperature: Sampling temperature
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
OpenAI-compatible request payload
|
|
89
|
+
"""
|
|
90
|
+
payload: dict[str, Any] = {
|
|
91
|
+
"model": model,
|
|
92
|
+
"messages": messages_history,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if task_description is not None:
|
|
96
|
+
payload["task_description"] = task_description
|
|
97
|
+
if task_id is not None:
|
|
98
|
+
payload["task_id"] = task_id
|
|
99
|
+
if temperature is not None:
|
|
100
|
+
payload["temperature"] = temperature
|
|
101
|
+
|
|
102
|
+
return payload
|
|
103
|
+
|
|
104
|
+
def _handle_response_error(
|
|
105
|
+
self, response: httpx.Response, response_data: dict
|
|
106
|
+
) -> None:
|
|
107
|
+
error_resp = ErrorResponse(**response_data)
|
|
108
|
+
if error_resp.error:
|
|
109
|
+
error_code = error_resp.error.code
|
|
110
|
+
error_msg = error_resp.error.message
|
|
111
|
+
logger.error(f"API Error [{error_code}]: {error_msg}")
|
|
112
|
+
|
|
113
|
+
# Map to specific exception types based on status code
|
|
114
|
+
exception_class = self._get_exception_class(response.status_code)
|
|
115
|
+
raise exception_class(
|
|
116
|
+
error_msg,
|
|
117
|
+
code=error_code,
|
|
118
|
+
status_code=response.status_code,
|
|
119
|
+
response=response,
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
# Error response without error details
|
|
123
|
+
logger.error(f"API error response without details: {response.status_code}")
|
|
124
|
+
exception_class = self._get_exception_class(response.status_code)
|
|
125
|
+
raise exception_class(
|
|
126
|
+
f"API error (status {response.status_code})",
|
|
127
|
+
status_code=response.status_code,
|
|
128
|
+
response=response,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def _get_exception_class(self, status_code: int) -> type[APIError]:
|
|
132
|
+
status_map = {
|
|
133
|
+
401: AuthenticationError,
|
|
134
|
+
404: NotFoundError,
|
|
135
|
+
422: ValidationError,
|
|
136
|
+
429: RateLimitError,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if status_code >= 500:
|
|
140
|
+
return ServerError
|
|
141
|
+
|
|
142
|
+
return status_map.get(status_code, APIError)
|
|
143
|
+
|
|
144
|
+
def _log_request_info(self, model: str, task_description: Any, task_id: Any):
|
|
145
|
+
logger.info(f"Making API request to /v2/message with model: {model}")
|
|
146
|
+
logger.debug(
|
|
147
|
+
f"Request includes task_description: {task_description is not None}, "
|
|
148
|
+
f"task_id: {task_id is not None}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def _build_user_message(
|
|
152
|
+
self, screenshot_url: str, instruction: str | None
|
|
153
|
+
) -> dict[str, Any]:
|
|
154
|
+
"""Build OpenAI-compatible user message with screenshot and optional instruction.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
screenshot_url: URL of uploaded screenshot
|
|
158
|
+
instruction: Optional text instruction
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
User message dict
|
|
162
|
+
"""
|
|
163
|
+
content = [{"type": "image_url", "image_url": {"url": screenshot_url}}]
|
|
164
|
+
if instruction:
|
|
165
|
+
content.append({"type": "text", "text": instruction})
|
|
166
|
+
return {"role": "user", "content": content}
|
|
167
|
+
|
|
168
|
+
def _prepare_message_payload(
|
|
169
|
+
self,
|
|
170
|
+
model: str,
|
|
171
|
+
upload_file_response: UploadFileResponse | None,
|
|
172
|
+
task_description: str | None,
|
|
173
|
+
task_id: str | None,
|
|
174
|
+
instruction: str | None,
|
|
175
|
+
messages_history: list | None,
|
|
176
|
+
temperature: float | None,
|
|
177
|
+
api_version: str | None,
|
|
178
|
+
screenshot_url: str | None = None,
|
|
179
|
+
) -> tuple[dict[str, str], dict[str, Any]]:
|
|
180
|
+
"""Prepare headers and payload for /v2/message request.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
model: Model to use
|
|
184
|
+
upload_file_response: Response from S3 upload (if screenshot was uploaded)
|
|
185
|
+
task_description: Task description
|
|
186
|
+
task_id: Task ID
|
|
187
|
+
instruction: Optional instruction
|
|
188
|
+
messages_history: Message history
|
|
189
|
+
temperature: Sampling temperature
|
|
190
|
+
api_version: API version
|
|
191
|
+
screenshot_url: Direct screenshot URL (alternative to upload_file_response)
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Tuple of (headers, payload)
|
|
195
|
+
"""
|
|
196
|
+
# Use provided screenshot_url or get from upload_file_response
|
|
197
|
+
if screenshot_url is None:
|
|
198
|
+
if upload_file_response is None:
|
|
199
|
+
raise ValueError(
|
|
200
|
+
"Either screenshot_url or upload_file_response must be provided"
|
|
201
|
+
)
|
|
202
|
+
screenshot_url = upload_file_response.download_url
|
|
203
|
+
|
|
204
|
+
# Build user message and append to history
|
|
205
|
+
if messages_history is None:
|
|
206
|
+
messages_history = []
|
|
207
|
+
user_message = self._build_user_message(screenshot_url, instruction)
|
|
208
|
+
messages_history.append(user_message)
|
|
209
|
+
|
|
210
|
+
# Build payload and headers
|
|
211
|
+
headers = self._build_headers(api_version)
|
|
212
|
+
payload = self._build_payload(
|
|
213
|
+
model=model,
|
|
214
|
+
messages_history=messages_history,
|
|
215
|
+
task_description=task_description,
|
|
216
|
+
task_id=task_id,
|
|
217
|
+
temperature=temperature,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return headers, payload
|
|
221
|
+
|
|
222
|
+
def _parse_response_json(self, response: httpx.Response) -> dict[str, Any]:
|
|
223
|
+
try:
|
|
224
|
+
return response.json()
|
|
225
|
+
except ValueError:
|
|
226
|
+
logger.error(f"Non-JSON API response: {response.status_code}")
|
|
227
|
+
raise APIError(
|
|
228
|
+
f"Invalid response format (status {response.status_code})",
|
|
229
|
+
status_code=response.status_code,
|
|
230
|
+
response=response,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def _process_response(self, response: httpx.Response) -> "LLMResponse":
|
|
234
|
+
response_data = self._parse_response_json(response)
|
|
235
|
+
|
|
236
|
+
# Check if it's an error response (non-200 status)
|
|
237
|
+
if response.status_code != 200:
|
|
238
|
+
self._handle_response_error(response, response_data)
|
|
239
|
+
|
|
240
|
+
# Parse successful response
|
|
241
|
+
result = LLMResponse(**response_data)
|
|
242
|
+
|
|
243
|
+
# Check if the response contains an error (even with 200 status)
|
|
244
|
+
if result.error:
|
|
245
|
+
logger.error(
|
|
246
|
+
f"API Error in response: [{result.error.code}]: {result.error.message}"
|
|
247
|
+
)
|
|
248
|
+
raise APIError(
|
|
249
|
+
result.error.message,
|
|
250
|
+
code=result.error.code,
|
|
251
|
+
status_code=200,
|
|
252
|
+
response=response,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
logger.info(
|
|
256
|
+
f"API request successful - task_id: {result.task_id}, "
|
|
257
|
+
f"complete: {result.is_complete}"
|
|
258
|
+
)
|
|
259
|
+
logger.debug(f"Response included {len(result.actions)} actions")
|
|
260
|
+
return result
|
|
261
|
+
|
|
262
|
+
def _process_upload_response(self, response: httpx.Response) -> UploadFileResponse:
|
|
263
|
+
"""Process response from /v1/file/upload endpoint.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
response: HTTP response from upload endpoint
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
UploadFileResponse with presigned URL
|
|
270
|
+
|
|
271
|
+
Raises:
|
|
272
|
+
RequestTimeoutError: If request times out
|
|
273
|
+
NetworkError: If network error occurs
|
|
274
|
+
APIError: If API returns error or invalid response
|
|
275
|
+
"""
|
|
276
|
+
try:
|
|
277
|
+
response_data = response.json()
|
|
278
|
+
upload_file_response = UploadFileResponse(**response_data)
|
|
279
|
+
logger.debug("Calling /v1/file/upload successful")
|
|
280
|
+
return upload_file_response
|
|
281
|
+
except ValueError:
|
|
282
|
+
logger.error(f"Non-JSON API response: {response.status_code}")
|
|
283
|
+
raise APIError(
|
|
284
|
+
f"Invalid response format (status {response.status_code})",
|
|
285
|
+
status_code=response.status_code,
|
|
286
|
+
response=response,
|
|
287
|
+
)
|
|
288
|
+
except KeyError as e:
|
|
289
|
+
logger.error(f"Invalid response: {response.status_code}")
|
|
290
|
+
raise APIError(
|
|
291
|
+
f"Invalid presigned S3 URL response: missing field {e}",
|
|
292
|
+
status_code=response.status_code,
|
|
293
|
+
response=response,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
def _handle_upload_http_errors(
|
|
297
|
+
self, e: Exception, response: httpx.Response | None = None
|
|
298
|
+
):
|
|
299
|
+
"""Handle HTTP errors during upload request.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
e: The exception that occurred
|
|
303
|
+
response: Optional HTTP response
|
|
304
|
+
|
|
305
|
+
Raises:
|
|
306
|
+
RequestTimeoutError: If request times out
|
|
307
|
+
NetworkError: If network error occurs
|
|
308
|
+
APIError: For other HTTP errors
|
|
309
|
+
"""
|
|
310
|
+
if isinstance(e, httpx.TimeoutException):
|
|
311
|
+
logger.error(f"Request timed out after {self.timeout} seconds")
|
|
312
|
+
raise RequestTimeoutError(
|
|
313
|
+
f"Request timed out after {self.timeout} seconds", e
|
|
314
|
+
)
|
|
315
|
+
elif isinstance(e, httpx.NetworkError):
|
|
316
|
+
logger.error(f"Network error: {e}")
|
|
317
|
+
raise NetworkError(f"Network error: {e}", e)
|
|
318
|
+
elif isinstance(e, httpx.HTTPStatusError) and response:
|
|
319
|
+
logger.warning(f"Invalid status code: {e}")
|
|
320
|
+
exception_class = self._get_exception_class(response.status_code)
|
|
321
|
+
raise exception_class(
|
|
322
|
+
f"API error (status {response.status_code})",
|
|
323
|
+
status_code=response.status_code,
|
|
324
|
+
response=response,
|
|
325
|
+
)
|
|
326
|
+
else:
|
|
327
|
+
raise
|
|
328
|
+
|
|
329
|
+
def _handle_s3_upload_error(
|
|
330
|
+
self, e: Exception, response: httpx.Response | None = None
|
|
331
|
+
):
|
|
332
|
+
"""Handle S3 upload errors.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
e: The exception that occurred
|
|
336
|
+
response: Optional HTTP response from S3
|
|
337
|
+
|
|
338
|
+
Raises:
|
|
339
|
+
APIError: Wrapping the S3 upload error
|
|
340
|
+
"""
|
|
341
|
+
logger.error(f"S3 upload failed: {e}")
|
|
342
|
+
status_code = response.status_code if response else 500
|
|
343
|
+
raise APIError(message=str(e), status_code=status_code, response=response)
|
|
344
|
+
|
|
345
|
+
def _prepare_worker_request(
|
|
346
|
+
self,
|
|
347
|
+
worker_id: str,
|
|
348
|
+
overall_todo: str,
|
|
349
|
+
task_description: str,
|
|
350
|
+
todos: list[dict],
|
|
351
|
+
history: list[dict] | None = None,
|
|
352
|
+
current_todo_index: int | None = None,
|
|
353
|
+
task_execution_summary: str | None = None,
|
|
354
|
+
current_screenshot: str | None = None,
|
|
355
|
+
current_subtask_instruction: str | None = None,
|
|
356
|
+
window_steps: list[dict] | None = None,
|
|
357
|
+
window_screenshots: list[str] | None = None,
|
|
358
|
+
result_screenshot: str | None = None,
|
|
359
|
+
prior_notes: str | None = None,
|
|
360
|
+
latest_todo_summary: str | None = None,
|
|
361
|
+
api_version: str | None = None,
|
|
362
|
+
) -> tuple[dict[str, Any], dict[str, str]]:
|
|
363
|
+
"""Prepare worker request with validation, payload, and headers.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
worker_id: One of "oagi_first", "oagi_follow", "oagi_task_summary"
|
|
367
|
+
overall_todo: Current todo description
|
|
368
|
+
task_description: Overall task description
|
|
369
|
+
todos: List of todo dicts with index, description, status, execution_summary
|
|
370
|
+
history: List of history dicts with todo_index, todo_description, action_count, summary, completed
|
|
371
|
+
current_todo_index: Index of current todo being executed
|
|
372
|
+
task_execution_summary: Summary of overall task execution
|
|
373
|
+
current_screenshot: Uploaded file UUID for screenshot (oagi_first)
|
|
374
|
+
current_subtask_instruction: Subtask instruction (oagi_follow)
|
|
375
|
+
window_steps: Action steps list (oagi_follow)
|
|
376
|
+
window_screenshots: Uploaded file UUIDs list (oagi_follow)
|
|
377
|
+
result_screenshot: Uploaded file UUID for result screenshot (oagi_follow)
|
|
378
|
+
prior_notes: Execution notes (oagi_follow)
|
|
379
|
+
latest_todo_summary: Latest summary (oagi_task_summary)
|
|
380
|
+
api_version: API version header
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
Tuple of (payload dict, headers dict)
|
|
384
|
+
|
|
385
|
+
Raises:
|
|
386
|
+
ValueError: If worker_id is invalid
|
|
387
|
+
"""
|
|
388
|
+
# Validate worker_id
|
|
389
|
+
valid_workers = {"oagi_first", "oagi_follow", "oagi_task_summary"}
|
|
390
|
+
if worker_id not in valid_workers:
|
|
391
|
+
raise ValueError(
|
|
392
|
+
f"Invalid worker_id '{worker_id}'. Must be one of: {valid_workers}"
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
logger.info(f"Calling /v1/generate with worker_id: {worker_id}")
|
|
396
|
+
|
|
397
|
+
# Build flattened payload (no oagi_data wrapper)
|
|
398
|
+
payload: dict[str, Any] = {
|
|
399
|
+
"external_worker_id": worker_id,
|
|
400
|
+
"overall_todo": overall_todo,
|
|
401
|
+
"task_description": task_description,
|
|
402
|
+
"todos": todos,
|
|
403
|
+
"history": history or [],
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
# Add optional memory fields
|
|
407
|
+
if current_todo_index is not None:
|
|
408
|
+
payload["current_todo_index"] = current_todo_index
|
|
409
|
+
if task_execution_summary is not None:
|
|
410
|
+
payload["task_execution_summary"] = task_execution_summary
|
|
411
|
+
|
|
412
|
+
# Add optional screenshot/worker-specific fields
|
|
413
|
+
if current_screenshot is not None:
|
|
414
|
+
payload["current_screenshot"] = current_screenshot
|
|
415
|
+
if current_subtask_instruction is not None:
|
|
416
|
+
payload["current_subtask_instruction"] = current_subtask_instruction
|
|
417
|
+
if window_steps is not None:
|
|
418
|
+
payload["window_steps"] = window_steps
|
|
419
|
+
if window_screenshots is not None:
|
|
420
|
+
payload["window_screenshots"] = window_screenshots
|
|
421
|
+
if result_screenshot is not None:
|
|
422
|
+
payload["result_screenshot"] = result_screenshot
|
|
423
|
+
if prior_notes is not None:
|
|
424
|
+
payload["prior_notes"] = prior_notes
|
|
425
|
+
if latest_todo_summary is not None:
|
|
426
|
+
payload["latest_todo_summary"] = latest_todo_summary
|
|
427
|
+
|
|
428
|
+
# Build headers
|
|
429
|
+
headers = self._build_headers(api_version)
|
|
430
|
+
|
|
431
|
+
return payload, headers
|
|
432
|
+
|
|
433
|
+
def _process_generate_response(self, response: httpx.Response) -> GenerateResponse:
|
|
434
|
+
"""Process response from /v1/generate endpoint.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
response: HTTP response from generate endpoint
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
GenerateResponse with LLM output
|
|
441
|
+
|
|
442
|
+
Raises:
|
|
443
|
+
APIError: If API returns error or invalid response
|
|
444
|
+
"""
|
|
445
|
+
response_data = self._parse_response_json(response)
|
|
446
|
+
|
|
447
|
+
# Check if it's an error response (non-200 status)
|
|
448
|
+
if response.status_code != 200:
|
|
449
|
+
self._handle_response_error(response, response_data)
|
|
450
|
+
|
|
451
|
+
# Parse successful response
|
|
452
|
+
result = GenerateResponse(**response_data)
|
|
453
|
+
|
|
454
|
+
logger.info(
|
|
455
|
+
f"Generate request successful - tokens: {result.prompt_tokens}+{result.completion_tokens}, "
|
|
456
|
+
)
|
|
457
|
+
return result
|