Compare commits
No commits in common. "ff1799cd984cb5540103a01dcfa90983535f20b3" and "365ab8dd79274cd39226d41401199379372955db" have entirely different histories.
ff1799cd98
...
365ab8dd79
7 changed files with 6 additions and 206 deletions
|
|
@ -11,7 +11,6 @@ REST API-обертка над `browser-use` RPC (`POST /run` в контейн
|
||||||
- `POST /api/browser/tasks` возвращает `202` и `task_id`
|
- `POST /api/browser/tasks` возвращает `202` и `task_id`
|
||||||
- `GET /api/browser/tasks/{task_id}` возвращает `queued/running/...`
|
- `GET /api/browser/tasks/{task_id}` возвращает `queued/running/...`
|
||||||
- `GET /api/browser/tasks/{task_id}/result` возвращает `202`, пока задача не завершена
|
- `GET /api/browser/tasks/{task_id}/result` возвращает `202`, пока задача не завершена
|
||||||
- `GET /api/browser/tasks/{task_id}/history` возвращает историю шагов агента
|
|
||||||
|
|
||||||
## Архитектура
|
## Архитектура
|
||||||
|
|
||||||
|
|
@ -103,34 +102,6 @@ Response `202`:
|
||||||
- `202` если задача еще `queued/running`
|
- `202` если задача еще `queued/running`
|
||||||
- `200` с финальным payload после завершения
|
- `200` с финальным payload после завершения
|
||||||
|
|
||||||
### `GET /api/browser/tasks/{task_id}/history`
|
|
||||||
|
|
||||||
- `202` если задача еще `queued/running`
|
|
||||||
- `200` с финальной историей шагов после завершения
|
|
||||||
|
|
||||||
Пример ответа `200`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"task_id": "53f54fa4c1f24219b3949d56b0457875",
|
|
||||||
"status": "succeeded",
|
|
||||||
"history": [
|
|
||||||
{
|
|
||||||
"step": 1,
|
|
||||||
"kind": "thought",
|
|
||||||
"content": "Open target page",
|
|
||||||
"data": {"value": "Open target page"}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"step": 2,
|
|
||||||
"kind": "action",
|
|
||||||
"content": "Click login",
|
|
||||||
"data": {"value": "Click login"}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Быстрый end-to-end пример
|
## Быстрый end-to-end пример
|
||||||
|
|
||||||
```zsh
|
```zsh
|
||||||
|
|
@ -146,5 +117,4 @@ TASK_ID=$(python -c "import json,sys;print(json.loads(sys.argv[1])['task_id'])"
|
||||||
|
|
||||||
curl -sS "http://localhost:8088/api/browser/tasks/$TASK_ID"
|
curl -sS "http://localhost:8088/api/browser/tasks/$TASK_ID"
|
||||||
curl -sS "http://localhost:8088/api/browser/tasks/$TASK_ID/result"
|
curl -sS "http://localhost:8088/api/browser/tasks/$TASK_ID/result"
|
||||||
curl -sS "http://localhost:8088/api/browser/tasks/$TASK_ID/history"
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -41,21 +41,3 @@ class BrowserTaskResultResponse(BaseModel):
|
||||||
result: str | None = Field(default=None, description="Итоговый текстовый результат")
|
result: str | None = Field(default=None, description="Итоговый текстовый результат")
|
||||||
error: str | None = Field(default=None, description="Текст ошибки, если выполнение не удалось")
|
error: str | None = Field(default=None, description="Текст ошибки, если выполнение не удалось")
|
||||||
raw_response: dict[str, Any] | None = Field(default=None, description="Сырой ответ от browser-use RPC")
|
raw_response: dict[str, Any] | None = Field(default=None, description="Сырой ответ от browser-use RPC")
|
||||||
|
|
||||||
|
|
||||||
class TaskHistoryEvent(BaseModel):
|
|
||||||
"""Одно действие/шаг в истории выполнения browser-use агента."""
|
|
||||||
|
|
||||||
step: int = Field(..., description="Порядковый номер события в истории")
|
|
||||||
kind: str = Field(..., description="Тип события (thought/action/error/system)")
|
|
||||||
content: str | None = Field(default=None, description="Краткое текстовое описание события")
|
|
||||||
data: dict[str, Any] = Field(default_factory=dict, description="Дополнительные структурированные данные")
|
|
||||||
|
|
||||||
|
|
||||||
class BrowserTaskHistoryResponse(BaseModel):
|
|
||||||
"""История действий агента для конкретной задачи."""
|
|
||||||
|
|
||||||
task_id: str
|
|
||||||
status: TaskStatus
|
|
||||||
history: list[TaskHistoryEvent] = Field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ class TaskRecord:
|
||||||
result: str | None = None
|
result: str | None = None
|
||||||
error: str | None = None
|
error: str | None = None
|
||||||
raw_response: dict[str, Any] | None = None
|
raw_response: dict[str, Any] | None = None
|
||||||
history: list[dict[str, Any]] = field(default_factory=list)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def execution_time(self) -> float:
|
def execution_time(self) -> float:
|
||||||
|
|
@ -62,7 +61,6 @@ class TaskStore:
|
||||||
raw_response: dict[str, Any] | None,
|
raw_response: dict[str, Any] | None,
|
||||||
error: str | None,
|
error: str | None,
|
||||||
result: str | None = None,
|
result: str | None = None,
|
||||||
history: list[dict[str, Any]] | None = None,
|
|
||||||
) -> TaskRecord | None:
|
) -> TaskRecord | None:
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
rec = self._tasks.get(task_id)
|
rec = self._tasks.get(task_id)
|
||||||
|
|
@ -74,6 +72,5 @@ class TaskStore:
|
||||||
raw_response.get("error") if isinstance(raw_response, dict) else None)
|
raw_response.get("error") if isinstance(raw_response, dict) else None)
|
||||||
rec.result = result if result is not None else (
|
rec.result = result if result is not None else (
|
||||||
raw_response.get("result") if isinstance(raw_response, dict) else None)
|
raw_response.get("result") if isinstance(raw_response, dict) else None)
|
||||||
rec.history = list(history or [])
|
|
||||||
rec.status = TaskStatus.succeeded if success else TaskStatus.failed
|
rec.status = TaskStatus.succeeded if success else TaskStatus.failed
|
||||||
return rec
|
return rec
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,9 @@ from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
from api.contracts.task_schemas import (
|
from api.contracts.task_schemas import (
|
||||||
BrowserTaskAcceptedResponse,
|
BrowserTaskAcceptedResponse,
|
||||||
BrowserTaskHistoryResponse,
|
|
||||||
BrowserTaskRequest,
|
BrowserTaskRequest,
|
||||||
BrowserTaskResultResponse,
|
BrowserTaskResultResponse,
|
||||||
BrowserTaskStatusResponse,
|
BrowserTaskStatusResponse,
|
||||||
TaskHistoryEvent,
|
|
||||||
)
|
)
|
||||||
from api.domain.task_status import TaskStatus
|
from api.domain.task_status import TaskStatus
|
||||||
from api.repositories.task_store import TaskRecord
|
from api.repositories.task_store import TaskRecord
|
||||||
|
|
@ -71,32 +69,6 @@ async def get_task_result(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tasks/{task_id}/history", response_model=BrowserTaskHistoryResponse)
|
|
||||||
async def get_task_history(
|
|
||||||
task_id: str,
|
|
||||||
service: TaskService = Depends(get_task_service),
|
|
||||||
) -> JSONResponse | BrowserTaskHistoryResponse:
|
|
||||||
rec = await service.get_task(task_id)
|
|
||||||
if rec is None:
|
|
||||||
raise HTTPException(status_code=404, detail="Task not found")
|
|
||||||
|
|
||||||
if rec.status in (TaskStatus.queued, TaskStatus.running):
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=202,
|
|
||||||
content={
|
|
||||||
"task_id": rec.task_id,
|
|
||||||
"status": rec.status.value,
|
|
||||||
"history": rec.history,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return BrowserTaskHistoryResponse(
|
|
||||||
task_id=rec.task_id,
|
|
||||||
status=rec.status,
|
|
||||||
history=_to_history_events(rec),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _to_status_response(rec: TaskRecord) -> BrowserTaskStatusResponse:
|
def _to_status_response(rec: TaskRecord) -> BrowserTaskStatusResponse:
|
||||||
return BrowserTaskStatusResponse(
|
return BrowserTaskStatusResponse(
|
||||||
task_id=rec.task_id,
|
task_id=rec.task_id,
|
||||||
|
|
@ -106,24 +78,3 @@ def _to_status_response(rec: TaskRecord) -> BrowserTaskStatusResponse:
|
||||||
finished_at=rec.finished_at,
|
finished_at=rec.finished_at,
|
||||||
error=rec.error,
|
error=rec.error,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _to_history_events(rec: TaskRecord) -> list[TaskHistoryEvent]:
|
|
||||||
events: list[TaskHistoryEvent] = []
|
|
||||||
for index, item in enumerate(rec.history, start=1):
|
|
||||||
kind = str(item.get("kind") or item.get("type") or "system")
|
|
||||||
content = item.get("content")
|
|
||||||
if content is not None:
|
|
||||||
content = str(content)
|
|
||||||
data = item.get("data")
|
|
||||||
if not isinstance(data, dict):
|
|
||||||
data = {}
|
|
||||||
|
|
||||||
step = item.get("step")
|
|
||||||
if not isinstance(step, int):
|
|
||||||
step = index
|
|
||||||
|
|
||||||
events.append(TaskHistoryEvent(step=step, kind=kind, content=content, data=data))
|
|
||||||
|
|
||||||
return events
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,6 @@ class TaskService:
|
||||||
raw_response=raw,
|
raw_response=raw,
|
||||||
error=None,
|
error=None,
|
||||||
result=raw.get("result") if isinstance(raw, dict) else None,
|
result=raw.get("result") if isinstance(raw, dict) else None,
|
||||||
history=self._extract_history(raw),
|
|
||||||
)
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await self._store.set_done(
|
await self._store.set_done(
|
||||||
|
|
@ -67,7 +66,6 @@ class TaskService:
|
||||||
success=False,
|
success=False,
|
||||||
raw_response=None,
|
raw_response=None,
|
||||||
error="Timeout exceeded",
|
error="Timeout exceeded",
|
||||||
history=None,
|
|
||||||
)
|
)
|
||||||
except BrowserRpcError as exc:
|
except BrowserRpcError as exc:
|
||||||
await self._store.set_done(
|
await self._store.set_done(
|
||||||
|
|
@ -75,7 +73,6 @@ class TaskService:
|
||||||
success=False,
|
success=False,
|
||||||
raw_response=None,
|
raw_response=None,
|
||||||
error=str(exc),
|
error=str(exc),
|
||||||
history=None,
|
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
await self._store.set_done(
|
await self._store.set_done(
|
||||||
|
|
@ -83,21 +80,4 @@ class TaskService:
|
||||||
success=False,
|
success=False,
|
||||||
raw_response=None,
|
raw_response=None,
|
||||||
error=f"Internal error: {exc}",
|
error=f"Internal error: {exc}",
|
||||||
history=None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _extract_history(raw: dict | None) -> list[dict]:
|
|
||||||
if not isinstance(raw, dict):
|
|
||||||
return []
|
|
||||||
|
|
||||||
events = raw.get("history")
|
|
||||||
if not isinstance(events, list):
|
|
||||||
return []
|
|
||||||
|
|
||||||
normalized: list[dict] = []
|
|
||||||
for event in events:
|
|
||||||
if isinstance(event, dict):
|
|
||||||
normalized.append(event)
|
|
||||||
return normalized
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Файл: assets/config.example.json
|
||||||
|
|
||||||
|
```json
|
||||||
{
|
{
|
||||||
"browser": {
|
"browser": {
|
||||||
"headless": true,
|
"headless": true,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import asyncio
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
from typing import Any
|
|
||||||
from urllib import error, request
|
from urllib import error, request
|
||||||
|
|
||||||
from browser_use import Agent, Browser, ChatOpenAI
|
from browser_use import Agent, Browser, ChatOpenAI
|
||||||
|
|
@ -37,7 +36,6 @@ async def run_browser_task(task):
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"result": history.final_result(),
|
"result": history.final_result(),
|
||||||
"history": _extract_history_events(history),
|
|
||||||
"browser_view": browser_view_url,
|
"browser_view": browser_view_url,
|
||||||
}
|
}
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
|
|
@ -49,90 +47,6 @@ async def run_browser_task(task):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _to_jsonable(value: Any) -> Any:
|
|
||||||
if value is None or isinstance(value, (str, int, float, bool)):
|
|
||||||
return value
|
|
||||||
if isinstance(value, dict):
|
|
||||||
return {str(key): _to_jsonable(val) for key, val in value.items()}
|
|
||||||
if isinstance(value, (list, tuple, set)):
|
|
||||||
return [_to_jsonable(item) for item in value]
|
|
||||||
|
|
||||||
for method_name in ("model_dump", "dict", "to_dict"):
|
|
||||||
method = getattr(value, method_name, None)
|
|
||||||
if callable(method):
|
|
||||||
try:
|
|
||||||
dumped = method()
|
|
||||||
return _to_jsonable(dumped)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return str(value)
|
|
||||||
|
|
||||||
|
|
||||||
def _call_history_items(history: Any, attr_name: str) -> list[Any]:
|
|
||||||
method = getattr(history, attr_name, None)
|
|
||||||
if not callable(method):
|
|
||||||
return []
|
|
||||||
|
|
||||||
try:
|
|
||||||
raw: Any = method()
|
|
||||||
except Exception:
|
|
||||||
return []
|
|
||||||
|
|
||||||
if raw is None:
|
|
||||||
return []
|
|
||||||
if isinstance(raw, list):
|
|
||||||
return raw
|
|
||||||
if isinstance(raw, (str, bytes, dict)):
|
|
||||||
return [raw]
|
|
||||||
|
|
||||||
try:
|
|
||||||
return list(raw)
|
|
||||||
except TypeError:
|
|
||||||
return [raw]
|
|
||||||
except Exception:
|
|
||||||
return [raw]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _extract_history_events(history: Any) -> list[dict[str, Any]]:
|
|
||||||
events: list[dict[str, Any]] = []
|
|
||||||
|
|
||||||
def append_many(kind: str, items: list[Any]) -> None:
|
|
||||||
if not items:
|
|
||||||
return
|
|
||||||
for item in items:
|
|
||||||
normalized = _to_jsonable(item)
|
|
||||||
payload = normalized if isinstance(normalized, dict) else {"value": normalized}
|
|
||||||
content = normalized if isinstance(normalized, str) else json.dumps(normalized, ensure_ascii=False)
|
|
||||||
events.append(
|
|
||||||
{
|
|
||||||
"step": len(events) + 1,
|
|
||||||
"kind": kind,
|
|
||||||
"content": content,
|
|
||||||
"data": payload,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
append_many("thought", _call_history_items(history, "model_thoughts"))
|
|
||||||
append_many("action", _call_history_items(history, "model_actions"))
|
|
||||||
append_many("error", _call_history_items(history, "errors"))
|
|
||||||
|
|
||||||
if events:
|
|
||||||
return events
|
|
||||||
|
|
||||||
fallback = _to_jsonable(history)
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"step": 1,
|
|
||||||
"kind": "system",
|
|
||||||
"content": fallback if isinstance(fallback, str) else json.dumps(fallback, ensure_ascii=False),
|
|
||||||
"data": fallback if isinstance(fallback, dict) else {"value": fallback},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class BrowserUseRPCHandler(BaseHTTPRequestHandler):
|
class BrowserUseRPCHandler(BaseHTTPRequestHandler):
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
if self.path != "/health":
|
if self.path != "/health":
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue