Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
43 changes: 27 additions & 16 deletions mesharc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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."""
Expand Down Expand Up @@ -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:
Expand All @@ -508,22 +513,22 @@ 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,
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, 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``,
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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))

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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]:
Expand Down
10 changes: 10 additions & 0 deletions tests/test_client_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading