diff --git a/CHANGELOG.md b/CHANGELOG.md index 747160d..f180b3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this package are recorded here. The format follows ### Added +- **The agent's saved logins need no picking.** `agent()`, `AgentRun.follow_up()` and `agent_batches` take `logins`: left out, the API reads each site the workspace has a saved login for signed in with that site's login (`"auto"`), and every other site signed out, so one run can read several sites signed in; `"none"`, or a list of login ids, changes that. `login_id` is the older form of a list of one. **This needs an API with `logins`**, on the API's feature branch. - **Follow-ups and conversations.** `AgentRun.follow_up(prompt, ...)` asks the next question in a run's conversation -- it takes what `agent()` takes, answers after everything asked and found before it, and may read again, free, any page the conversation read -- and returns its `AgentRun`. `conversations(q=None, limit=25)` walks the workspace's conversations, the latest message first; `conversation(id)` reads one with every run in it. A message that is not the conversation's latest is a 409 `conflict`. **These need an API with follow-ups**, on the API's feature branch, not yet on mesharc.dev. - **`agent()` takes `login_id=`, `data_connections=` and `proposals=`**: a saved login its site is read with; up to two of the workspace's data-provider connections (People Data Labs, Financial Modeling Prep), which the agent looks company and financial data up with on your own key, at no MeshArc credits, each record one of its `sources` as `{url, title, provider}`; and leave to propose things to watch, for a member to approve. Each is sent only when given. `trace()` knows the `lookup` step. **These need an API with saved logins, data providers and proposals**, on the API's feature branch. - **`arc.agent_batches`: the agent over every row of a table.** `preview`, `start`, `get`, `rows`, `wait`, `list`, `cancel` and `export` -- one agent run a row, its prompt and starting URLs filled from the row's columns as `{{column}}`, its answer the row's new columns; `max_credits_per_row` caps each row and the batch never spends more than rows x that cap. **This needs an API with `/agent/batch`**, on the API's feature branch. diff --git a/mesharc/__init__.py b/mesharc/__init__.py index a60ea90..e45767c 100644 --- a/mesharc/__init__.py +++ b/mesharc/__init__.py @@ -112,11 +112,11 @@ def _some(**fields: Any) -> Json: def _agent_body(prompt: str, urls: Optional[Iterable[str]], schema: Optional[Json], max_credits: Optional[int], max_steps: Optional[int], allowed_domains: Optional[Iterable[str]], webhook: Union[str, Json, None], connection_id: Optional[str], login_id: Optional[str], data_connections: Optional[Iterable[str]], - proposals: Optional[bool], timeout_s: float) -> Json: + proposals: Optional[bool], timeout_s: float, logins: Union[str, Iterable[str], None] = None) -> Json: """A POST /agent body (and a follow-up's) in the API's own names, naming only what was given.""" body: Json = {"prompt": prompt} body.update(_some(schema=schema, maxCredits=max_credits, maxSteps=max_steps, webhook=webhook, - connectionId=connection_id, loginId=login_id, proposals=proposals)) + connectionId=connection_id, loginId=login_id, proposals=proposals, logins=_logins(logins))) if urls is not None: body["urls"] = list(urls) if allowed_domains is not None: @@ -128,6 +128,11 @@ def _agent_body(prompt: str, urls: Optional[Iterable[str]], schema: Optional[Jso return body +def _logins(logins: Union[str, Iterable[str], None]) -> Union[str, List[str], None]: + """``logins`` as the API takes it: "auto", "none", or a list of login ids.""" + return logins if logins is None or isinstance(logins, str) else list(logins) + + def _offset_rows(http: Any, path: str, params: Dict[str, Any], limit: int) -> Iterator[Json]: """Every row of an offset-paged list, ``limit`` at a time, until a page comes back short.""" @@ -490,11 +495,11 @@ def _body(prompt: str, csv: Optional[str], rows: Optional[List[Json]], max_credi urls: Optional[Iterable[str]], schema: Optional[Json], max_steps: Optional[int], allowed_domains: Optional[Iterable[str]], concurrency: Optional[int], name: Optional[str], connection_id: Optional[str], login_id: Optional[str], - data_connections: Optional[Iterable[str]]) -> Json: + data_connections: Optional[Iterable[str]], logins: Union[str, Iterable[str], None] = None) -> Json: body: Json = {"prompt": prompt} body.update(_some(csv=csv, rows=rows, maxCreditsPerRow=max_credits_per_row, schema=schema, maxSteps=max_steps, concurrency=concurrency, name=name, connectionId=connection_id, - loginId=login_id)) + loginId=login_id, logins=_logins(logins))) if urls is not None: body["urls"] = list(urls) if allowed_domains is not None: @@ -508,14 +513,14 @@ def preview(self, prompt: str, csv: Optional[str] = None, rows: Optional[List[Js schema: Optional[Json] = None, max_steps: Optional[int] = None, allowed_domains: Optional[Iterable[str]] = None, concurrency: Optional[int] = None, name: Optional[str] = None, connection_id: Optional[str] = None, login_id: Optional[str] = None, - data_connections: Optional[Iterable[str]] = None) -> Json: + data_connections: Optional[Iterable[str]] = None, logins: Union[str, Iterable[str], None] = None) -> Json: """What ``start`` would do, without starting it or spending anything: the rows that can run and those that cannot (and why), the first prompts as their rows will ask them, and an estimate against the balance.""" return self._h("POST", "/agent/batch/preview", json=self._body( prompt, csv, rows, max_credits_per_row, urls, schema, max_steps, allowed_domains, concurrency, name, - connection_id, login_id, data_connections)) + connection_id, login_id, data_connections, logins)) def start(self, prompt: str, csv: Optional[str] = None, rows: Optional[List[Json]] = None, max_credits_per_row: Optional[int] = None, urls: Optional[Iterable[str]] = None, @@ -523,7 +528,7 @@ def start(self, prompt: str, csv: Optional[str] = None, rows: Optional[List[Json allowed_domains: Optional[Iterable[str]] = None, concurrency: Optional[int] = None, name: Optional[str] = None, connection_id: Optional[str] = None, login_id: Optional[str] = None, data_connections: Optional[Iterable[str]] = None, webhook: Union[str, Json, None] = None, - idempotency_key: Optional[str] = None) -> Json: + idempotency_key: Optional[str] = None, logins: Union[str, Iterable[str], None] = None) -> Json: """Start one agent run a row of the table. The table is ``csv`` (its first line names the columns) or ``rows``, @@ -532,15 +537,15 @@ def start(self, prompt: str, csv: Optional[str] = None, rows: Optional[List[Json columns. ``max_credits_per_row`` is required by the API. Without ``allowed_domains`` each row is held to its own URLs' sites; ``[]`` lifts the limit. ``concurrency`` rows run at once (3 by default). - ``login_id`` and ``data_connections`` are as ``agent()`` takes them, - for every row. ``webhook`` is told ``agent.batch.finished`` once; its + ``logins``, ``login_id`` and ``data_connections`` are as ``agent()`` + takes them, for every row. ``webhook`` is told ``agent.batch.finished`` once; its secret comes back once, as ``webhookSecret``. Spends credits and needs a key that can write. Returns the batch, with ``counts`` by row status; ``wait()`` waits for it. """ body = self._body(prompt, csv, rows, max_credits_per_row, urls, schema, max_steps, allowed_domains, - concurrency, name, connection_id, login_id, data_connections) + concurrency, name, connection_id, login_id, data_connections, logins) if webhook is not None: body["webhook"] = webhook return self._h("POST", "/agent/batch", json=body, idempotency_key=idempotency_key) @@ -1010,7 +1015,8 @@ def follow_up(self, prompt: str, urls: Optional[Iterable[str]] = None, schema: O allowed_domains: Optional[Iterable[str]] = None, webhook: Union[str, Json, None] = None, connection_id: Optional[str] = None, login_id: Optional[str] = None, data_connections: Optional[Iterable[str]] = None, proposals: Optional[bool] = None, - timeout_s: float = 0, idempotency_key: Optional[str] = None) -> AgentRun: + timeout_s: float = 0, idempotency_key: Optional[str] = None, + logins: Union[str, Iterable[str], None] = None) -> AgentRun: """Ask a follow-up in this run's conversation; returns its ``AgentRun``. It answers after everything asked and found in the conversation @@ -1022,7 +1028,7 @@ def follow_up(self, prompt: str, urls: Optional[Iterable[str]] = None, schema: O keep date. """ body = _agent_body(prompt, urls, schema, max_credits, max_steps, allowed_domains, webhook, connection_id, - login_id, data_connections, proposals, timeout_s) + login_id, data_connections, proposals, timeout_s, logins) return AgentRun(self._h, self._h("POST", f"/agent/{self.id}/follow-up", json=body, idempotency_key=idempotency_key)) @@ -1334,7 +1340,8 @@ def agent(self, prompt: str, urls: Optional[Iterable[str]] = None, schema: Optio allowed_domains: Optional[Iterable[str]] = None, webhook: Union[str, Json, None] = None, connection_id: Optional[str] = None, timeout_s: float = 0, idempotency_key: Optional[str] = None, login_id: Optional[str] = None, - data_connections: Optional[Iterable[str]] = None, proposals: Optional[bool] = None) -> AgentRun: + data_connections: Optional[Iterable[str]] = None, proposals: Optional[bool] = None, + logins: Union[str, Iterable[str], None] = None) -> AgentRun: """Hand a question to the agent: it searches, reads pages and answers. Needs an API key that can write, and spends credits: pages as they @@ -1355,8 +1362,12 @@ def agent(self, prompt: str, urls: Optional[Iterable[str]] = None, schema: Optio secret its messages are signed with comes back once, as ``AgentRun.webhook_secret``. - ``login_id`` is one of the workspace's saved logins: pages on its - site are read signed in, every other site signed out. + The workspace's saved logins need no picking: each page on a site + with one is read signed in with that site's login, every other page + signed out, so one run can read several sites signed in. ``logins`` + changes that -- ``"none"``, or a list of login ids to keep it to; + left out, the API's ``"auto"`` -- and ``login_id`` is the older form + of a list of one. ``data_connections`` names up to two of the workspace's data providers (one per provider): the agent may then look up company data (People Data Labs) or financial data (Financial Modeling Prep) @@ -1370,7 +1381,7 @@ def agent(self, prompt: str, urls: Optional[Iterable[str]] = None, schema: Optio asks the next question in the same conversation. """ body = _agent_body(prompt, urls, schema, max_credits, max_steps, allowed_domains, webhook, connection_id, - login_id, data_connections, proposals, timeout_s) + login_id, data_connections, proposals, timeout_s, logins) return AgentRun(self._h, self._h("POST", "/agent", json=body, idempotency_key=idempotency_key)) def conversations(self, q: Optional[str] = None, limit: int = 25) -> Iterator[Json]: diff --git a/tests/test_client_workspace.py b/tests/test_client_workspace.py index f3cf601..c044422 100644 --- a/tests/test_client_workspace.py +++ b/tests/test_client_workspace.py @@ -29,6 +29,16 @@ def test_an_agent_run_names_its_login_data_providers_and_proposals_only_when_giv assert body(arc.calls[1]) == {"prompt": "p", "loginId": "L1", "dataConnections": ["c1", "c2"], "proposals": True} +def test_logins_need_no_picking_and_are_sent_only_to_change_that(): + arc = scripted(run(), run(id="r2"), run(id="r3"), {"valid": 1}) + arc.agent("p") + arc.agent("p", logins="none") + arc.agent("p", logins=(i for i in ["l1", "l2"])) + arc.agent_batches.preview("about {{co}}", csv="co\nAcme\n", max_credits_per_row=10, logins=["l1"]) + assert "logins" not in body(arc.calls[0]) + assert [body(c)["logins"] for c in arc.calls[1:]] == ["none", ["l1", "l2"], ["l1"]] + + def test_a_follow_up_posts_its_own_request_to_the_runs_conversation(): arc = scripted(run(status="done"), run(id="r2", followsRunId="r1")) first = arc.get_agent("r1")