diff --git a/api/README.md b/api/README.md index bfe4f16e..74dbed10 100644 --- a/api/README.md +++ b/api/README.md @@ -11,7 +11,6 @@ REST API-обертка над `browser-use` RPC (`POST /run` в контейн - `POST /api/browser/tasks` возвращает `202` и `task_id` - `GET /api/browser/tasks/{task_id}` возвращает `queued/running/...` - `GET /api/browser/tasks/{task_id}/result` возвращает `202`, пока задача не завершена -- `GET /api/browser/tasks/{task_id}/history` возвращает историю шагов агента ## Архитектура @@ -103,34 +102,6 @@ Response `202`: - `202` если задача еще `queued/running` - `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 пример ```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/result" -curl -sS "http://localhost:8088/api/browser/tasks/$TASK_ID/history" ``` diff --git a/api/contracts/task_schemas.py b/api/contracts/task_schemas.py index 9cbc6865..bcad3cbe 100644 --- a/api/contracts/task_schemas.py +++ b/api/contracts/task_schemas.py @@ -41,21 +41,3 @@ class BrowserTaskResultResponse(BaseModel): result: 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") - - -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) - diff --git a/api/repositories/task_store.py b/api/repositories/task_store.py index 3467a176..bc66cd18 100644 --- a/api/repositories/task_store.py +++ b/api/repositories/task_store.py @@ -20,7 +20,6 @@ class TaskRecord: result: str | None = None error: str | None = None raw_response: dict[str, Any] | None = None - history: list[dict[str, Any]] = field(default_factory=list) @property def execution_time(self) -> float: @@ -62,7 +61,6 @@ class TaskStore: raw_response: dict[str, Any] | None, error: str | None, result: str | None = None, - history: list[dict[str, Any]] | None = None, ) -> TaskRecord | None: async with self._lock: rec = self._tasks.get(task_id) @@ -74,6 +72,5 @@ class TaskStore: raw_response.get("error") if isinstance(raw_response, dict) else None) rec.result = result if result is not None else ( 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 return rec diff --git a/api/routes/tasks.py b/api/routes/tasks.py index 90a3a989..94ed6238 100644 --- a/api/routes/tasks.py +++ b/api/routes/tasks.py @@ -3,11 +3,9 @@ from fastapi.responses import JSONResponse from api.contracts.task_schemas import ( BrowserTaskAcceptedResponse, - BrowserTaskHistoryResponse, BrowserTaskRequest, BrowserTaskResultResponse, BrowserTaskStatusResponse, - TaskHistoryEvent, ) from api.domain.task_status import TaskStatus 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: return BrowserTaskStatusResponse( task_id=rec.task_id, @@ -106,24 +78,3 @@ def _to_status_response(rec: TaskRecord) -> BrowserTaskStatusResponse: finished_at=rec.finished_at, 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 - diff --git a/api/services/task_service.py b/api/services/task_service.py index f331ff1b..97fc6b39 100644 --- a/api/services/task_service.py +++ b/api/services/task_service.py @@ -59,7 +59,6 @@ class TaskService: raw_response=raw, error=None, result=raw.get("result") if isinstance(raw, dict) else None, - history=self._extract_history(raw), ) except asyncio.TimeoutError: await self._store.set_done( @@ -67,7 +66,6 @@ class TaskService: success=False, raw_response=None, error="Timeout exceeded", - history=None, ) except BrowserRpcError as exc: await self._store.set_done( @@ -75,7 +73,6 @@ class TaskService: success=False, raw_response=None, error=str(exc), - history=None, ) except Exception as exc: await self._store.set_done( @@ -83,21 +80,4 @@ class TaskService: success=False, raw_response=None, 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 - diff --git a/assets/config.example.json b/assets/config.example.json index a76d59f8..ae331184 100644 --- a/assets/config.example.json +++ b/assets/config.example.json @@ -1,3 +1,9 @@ + +--- + +## ⚙️ Файл: assets/config.example.json + +```json { "browser": { "headless": true, diff --git a/browser_env/browser_use_runner.py b/browser_env/browser_use_runner.py index f54a9ce6..08ed6b42 100644 --- a/browser_env/browser_use_runner.py +++ b/browser_env/browser_use_runner.py @@ -2,7 +2,6 @@ import asyncio import json import os from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -from typing import Any from urllib import error, request from browser_use import Agent, Browser, ChatOpenAI @@ -37,7 +36,6 @@ async def run_browser_task(task): return { "success": True, "result": history.final_result(), - "history": _extract_history_events(history), "browser_view": browser_view_url, } except Exception as err: @@ -49,90 +47,6 @@ async def run_browser_task(task): 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): def do_GET(self): if self.path != "/health":