feat: add guided exploration and share user-started MCP backend
This commit is contained in:
1 parent
f86db1ced6
commit
ee256ad7ce
144 files changed
+8625
-2075
No files matched your search
@@ -44,6 +44,28 @@ and bearer-token authentication and exposes only task-related APIs.
|
||||
|
||||
## Usage
|
||||
|
||||
For interactive case design, use `client.exploration` with the same actions as the
|
||||
web workspace and `mobile_exploration`. Use a stable request ID when retrying a write:
|
||||
|
||||
```python
|
||||
session = await client.exploration(
|
||||
"create",
|
||||
request_id="explore-settings-create",
|
||||
payload={"goal": "Explore the Settings search entry; ask when expectations are unclear"},
|
||||
)
|
||||
await client.exploration(
|
||||
"start",
|
||||
exploration_id=session["id"],
|
||||
request_id="explore-settings-start",
|
||||
)
|
||||
state = await client.exploration("get", exploration_id=session["id"])
|
||||
```
|
||||
|
||||
The default exploration model is `gpt-6.1-sol / high`. Timeouts preserve progress
|
||||
and pause; `completed` does not imply a passing test. Read `runs[].test_result`.
|
||||
See [exploration modes and actions](../../docs/exploration-mode.md) for manual
|
||||
demonstrations, versioned cases and reruns.
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
@@ -14,6 +14,7 @@ import asyncio
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
import re
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, Literal, Protocol
|
||||
|
||||
@@ -371,6 +372,75 @@ class ArtemisClient:
|
||||
json_body=json_body,
|
||||
)
|
||||
|
||||
async def exploration(
|
||||
self,
|
||||
action: str,
|
||||
*,
|
||||
exploration_id: str | None = None,
|
||||
request_id: str | None = None,
|
||||
payload: Mapping[str, Any] | None = None,
|
||||
demonstration_id: str | None = None,
|
||||
after: int = 0,
|
||||
version: int | None = None,
|
||||
) -> Any:
|
||||
"""Use the shared guided-exploration API; queries never start device work.
|
||||
|
||||
Actions: list/get/case, create/start/message/pause/continue/stop,
|
||||
demonstrate/demo_finish/demo_extend/demo_cancel, save_case/generate/validate/batch.
|
||||
Mutations require a stable request_id except pause/stop. Generate returns a
|
||||
synthesis job; poll get.generations. Batch creates a draft only.
|
||||
"""
|
||||
|
||||
def identifier(value):
|
||||
if not isinstance(value, str) or not re.fullmatch(r"[A-Za-z0-9_-]{1,100}", value):
|
||||
raise ValueError("Invalid exploration/demonstration identifier")
|
||||
return value
|
||||
|
||||
base = "/api/explorations"
|
||||
if action in {"list", "get", "case"}:
|
||||
if payload or request_id:
|
||||
raise ValueError("Read operations cannot contain mutation fields")
|
||||
if action == "list":
|
||||
return await self._request("GET", base)
|
||||
path = f"{base}/{identifier(exploration_id)}"
|
||||
if action == "get":
|
||||
if after < 0:
|
||||
raise ValueError("after must be nonnegative")
|
||||
return await self._request("GET", f"{path}?after={int(after)}")
|
||||
if version is not None and version < 1:
|
||||
raise ValueError("version must be positive")
|
||||
return await self._request(
|
||||
"GET", path + "/case" + (f"?version={int(version)}" if version is not None else "")
|
||||
)
|
||||
if action not in {"pause", "stop"} and not (request_id and request_id.strip()):
|
||||
raise ValueError("Mutation requires a stable request_id")
|
||||
body = dict(payload or {})
|
||||
if "request_id" in body:
|
||||
raise ValueError("Use the separate request_id argument")
|
||||
if request_id:
|
||||
body["request_id"] = request_id
|
||||
if action == "create":
|
||||
return await self._request("POST", base, json_body=body)
|
||||
suffix = {
|
||||
"start": "start",
|
||||
"message": "messages",
|
||||
"pause": "pause",
|
||||
"continue": "continue",
|
||||
"stop": "stop",
|
||||
"demonstrate": "demonstrations",
|
||||
"save_case": "case",
|
||||
"generate": "generate",
|
||||
"validate": "validate",
|
||||
"batch": "batch",
|
||||
}.get(action)
|
||||
if action in {"demo_finish", "demo_extend", "demo_cancel"}:
|
||||
suffix = f"demonstrations/{identifier(demonstration_id)}/{action.removeprefix('demo_')}"
|
||||
if suffix is None:
|
||||
raise ValueError("Unsupported exploration action")
|
||||
return await self._request(
|
||||
"POST", f"{base}/{identifier(exploration_id)}/{suffix}", json_body=body
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _mapping(payload: Any, *, endpoint: str) -> Mapping[str, Any]:
|
||||
if not isinstance(payload, Mapping):
|
||||
|
||||
@@ -56,6 +56,25 @@ class ArtemisClientTests(unittest.IsolatedAsyncioTestCase):
|
||||
transport=self.transport,
|
||||
)
|
||||
|
||||
async def test_exploration_queries_and_mutations_use_shared_api(self) -> None:
|
||||
self.transport.add("GET", "/api/explorations/session?after=2", {"id": "session"})
|
||||
self.transport.add(
|
||||
"POST", "/api/explorations/session/demonstrations/demo/finish", {"status": "analyzing"}
|
||||
)
|
||||
await self.client.exploration("get", exploration_id="session", after=2)
|
||||
await self.client.exploration(
|
||||
"demo_finish",
|
||||
exploration_id="session",
|
||||
demonstration_id="demo",
|
||||
request_id="finish-once",
|
||||
)
|
||||
self.assertEqual(self.transport.calls[-1][2], {"request_id": "finish-once"})
|
||||
self.assertEqual(self.transport.calls[0][0], "GET")
|
||||
with self.assertRaises(ValueError):
|
||||
await self.client.exploration("start", exploration_id="session")
|
||||
with self.assertRaises(ValueError):
|
||||
await self.client.exploration("get", exploration_id="../private")
|
||||
|
||||
async def test_submit_sends_idempotent_session_id(self) -> None:
|
||||
task_id = "00000000-0000-4000-8000-000000000123"
|
||||
self.transport.add(
|
||||
|
||||
Reference in new issue
Block a user