parcha-agentrun 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- parcha_agentrun-0.2.0/.gitignore +11 -0
- parcha_agentrun-0.2.0/CHANGELOG.md +20 -0
- parcha_agentrun-0.2.0/PKG-INFO +243 -0
- parcha_agentrun-0.2.0/README.md +218 -0
- parcha_agentrun-0.2.0/pyproject.toml +81 -0
- parcha_agentrun-0.2.0/scripts/smoke_staging.py +61 -0
- parcha_agentrun-0.2.0/scripts/update_contract.py +43 -0
- parcha_agentrun-0.2.0/src/agentrun/__init__.py +129 -0
- parcha_agentrun-0.2.0/src/agentrun/_batch.py +86 -0
- parcha_agentrun-0.2.0/src/agentrun/_http.py +184 -0
- parcha_agentrun-0.2.0/src/agentrun/_ops.py +451 -0
- parcha_agentrun-0.2.0/src/agentrun/_poll.py +61 -0
- parcha_agentrun-0.2.0/src/agentrun/_version.py +1 -0
- parcha_agentrun-0.2.0/src/agentrun/agent.py +833 -0
- parcha_agentrun-0.2.0/src/agentrun/client.py +695 -0
- parcha_agentrun-0.2.0/src/agentrun/errors.py +164 -0
- parcha_agentrun-0.2.0/src/agentrun/models.py +498 -0
- parcha_agentrun-0.2.0/src/agentrun/py.typed +0 -0
- parcha_agentrun-0.2.0/tests/__init__.py +0 -0
- parcha_agentrun-0.2.0/tests/conftest.py +339 -0
- parcha_agentrun-0.2.0/tests/fixtures/public-v2.json +11964 -0
- parcha_agentrun-0.2.0/tests/test_agent.py +337 -0
- parcha_agentrun-0.2.0/tests/test_batches_loops_campaigns.py +215 -0
- parcha_agentrun-0.2.0/tests/test_contract.py +159 -0
- parcha_agentrun-0.2.0/tests/test_errors.py +111 -0
- parcha_agentrun-0.2.0/tests/test_packaged_contract.py +127 -0
- parcha_agentrun-0.2.0/tests/test_statuses.py +117 -0
- parcha_agentrun-0.2.0/tests/typing/__init__.py +0 -0
- parcha_agentrun-0.2.0/tests/typing/test_types.py +55 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
First release of the rewritten SDK. Requires the unified v2 backend.
|
|
6
|
+
|
|
7
|
+
- Published as `parcha-agentrun` on PyPI. The module is still `agentrun`. PyPI rejected `agentrun-ai` as too similar to an existing project.
|
|
8
|
+
|
|
9
|
+
- Targets the unified v2 API. Default base URL is `https://api.grep.ai/api/v2`.
|
|
10
|
+
- `AgentRun` and `AsyncAgentRun` clients cover agents, builds, runs, batches, loops, skills, integrations, campaigns, and usage.
|
|
11
|
+
- `Agent` and `AsyncAgent` wrap one agent: define it, build it from a description, or reuse it by ID.
|
|
12
|
+
- Typed responses as pydantic models. Ships `py.typed`; checked with pyright strict and mypy strict.
|
|
13
|
+
- Transport is `httpx2` with `anyio` for async. No automatic retries; idempotency keys are passed through.
|
|
14
|
+
- Errors: one class per HTTP status family, plus `InvalidResponseError`, `BuildError`, and `PollTimeoutError`.
|
|
15
|
+
- CSV batches through `BatchCSV`, with the same limits the API enforces.
|
|
16
|
+
- Run, batch, and row statuses are mapped to a short SDK vocabulary; the original value is kept in `raw_status`.
|
|
17
|
+
- Request limits from the API contract (skill lengths, `max_cost_usd`, batch name) are checked locally.
|
|
18
|
+
- Error messages carry the API's reason and request ID from its `{"error": {...}}` envelope.
|
|
19
|
+
- Verified live against staging: run, idempotent replay, wait, cancel, and 404 handling.
|
|
20
|
+
- `tests/fixtures/public-v2.json` carries the backend's OpenAPI contract; every SDK request is validated against it in tests.
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: parcha-agentrun
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python SDK for AgentRun: create agents, run them, and collect results
|
|
5
|
+
Project-URL: Homepage, https://agentrun.ai
|
|
6
|
+
Project-URL: Documentation, https://agentrun.ai/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/Parcha-ai/agentrun-py
|
|
8
|
+
Project-URL: Issues, https://github.com/Parcha-ai/agentrun-py/issues
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: anyio<5,>=4.4
|
|
21
|
+
Requires-Dist: httpx2<3,>=2.12
|
|
22
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
23
|
+
Requires-Dist: typing-extensions>=4.12
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# AgentRun Python SDK
|
|
27
|
+
|
|
28
|
+
Create agents, run them on your inputs, and collect the results. Sync and async clients, typed responses, Python 3.10 or newer.
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
pip install parcha-agentrun
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The package on PyPI is `parcha-agentrun`. The module you import is `agentrun`.
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
Set `AGENTRUN_API_KEY` in your environment, then:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from agentrun import Agent
|
|
42
|
+
|
|
43
|
+
agent = Agent(
|
|
44
|
+
name="Company verifier",
|
|
45
|
+
instructions="Verify the company and cite primary sources.",
|
|
46
|
+
)
|
|
47
|
+
run = agent.run("Verify acme.example")
|
|
48
|
+
result = agent.wait(run.id)
|
|
49
|
+
|
|
50
|
+
print(result.status) # "completed", "failed", "cancelled", or "blocked"
|
|
51
|
+
print(result.markdown) # the report, or None
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`Agent` saves itself on the first call that needs it. Save `agent.id` and pass `Agent(agent_id=...)` next time to reuse it. Two things to know about `wait`. It returns for every terminal status, so check `result.status` before using the report. It gives up after 10 minutes by default and raises `PollTimeoutError`, but the run keeps going on the server. Call `agent.get_run(run.id)` later to fetch it.
|
|
55
|
+
|
|
56
|
+
## Build an agent from a description
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
agent = Agent.create("Research companies and cite sources.")
|
|
60
|
+
# Save agent.build_id if this process might stop.
|
|
61
|
+
agent.wait_until_ready() # builds can take a few minutes
|
|
62
|
+
run = agent.run("Research acme.example")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Resume an unfinished build with `Agent(build_id=...)`. Any run method waits for the build first. A build that fails raises `BuildError`; read `error.build.error` for the reason.
|
|
66
|
+
|
|
67
|
+
## Async
|
|
68
|
+
|
|
69
|
+
Every class has an async twin with the same methods. Works under asyncio and trio.
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from agentrun import AsyncAgent
|
|
73
|
+
|
|
74
|
+
async def main() -> None:
|
|
75
|
+
agent = AsyncAgent(agent_id="agent-123")
|
|
76
|
+
run = await agent.run("Verify acme.example")
|
|
77
|
+
result = await agent.wait(run.id)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Statuses
|
|
81
|
+
|
|
82
|
+
The API reports progress in its own vocabulary. The SDK maps it to a short list so `is_done` and your code stay simple. The original value is always in `raw_status`.
|
|
83
|
+
|
|
84
|
+
| `run.status` | Means | Done |
|
|
85
|
+
| --- | --- | --- |
|
|
86
|
+
| `queued` | Accepted, not started. | no |
|
|
87
|
+
| `moderation` | Content check in progress. | no |
|
|
88
|
+
| `running` | Working. | no |
|
|
89
|
+
| `blocked` | Needs your input, or content was rejected. | yes |
|
|
90
|
+
| `completed` | Report is ready. | yes |
|
|
91
|
+
| `failed` | Ran and failed, or never ran. | yes |
|
|
92
|
+
| `cancelled` | Cancelled, deleted, or superseded. | yes |
|
|
93
|
+
|
|
94
|
+
Batches use `queued`, `running`, `completed`, `completed_with_errors`, `failed`, and `cancelled`. Batch rows use `queued`, `running`, `completed`, and `failed`. A status the SDK does not recognise passes through unchanged.
|
|
95
|
+
|
|
96
|
+
## Structured output
|
|
97
|
+
|
|
98
|
+
Pass a pydantic model or a JSON schema dict as `output_schema`. The result's `structured_output` is a dict; `output_as` validates it into your model.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from pydantic import BaseModel
|
|
102
|
+
|
|
103
|
+
class Finding(BaseModel):
|
|
104
|
+
verified: bool
|
|
105
|
+
evidence: str
|
|
106
|
+
|
|
107
|
+
run = agent.run("Verify acme.example", output_schema=Finding)
|
|
108
|
+
finding = agent.wait(run.id).output_as(Finding)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Batches
|
|
112
|
+
|
|
113
|
+
Run up to 100 inputs at once. The SDK checks the limits before sending anything.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
batch = agent.batch(
|
|
117
|
+
["Verify acme.example", "Verify beta.example"],
|
|
118
|
+
name="Q3 checks",
|
|
119
|
+
max_concurrent=5,
|
|
120
|
+
idempotency_key="q3-checks-001",
|
|
121
|
+
)
|
|
122
|
+
done = agent.wait_batch(batch.id)
|
|
123
|
+
for row in agent.get_batch_results(batch.id).results:
|
|
124
|
+
print(row.question, row.status, row.run_id)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
A CSV works too. Name the column that holds each input. The other columns come back as `row.row_data`.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from agentrun import BatchCSV
|
|
131
|
+
|
|
132
|
+
batch = agent.batch(BatchCSV("companies.csv", input_column="website"))
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`BatchCSV` accepts a path, CSV text, bytes, or an open file. Files are limited to 1 MB.
|
|
136
|
+
|
|
137
|
+
`wait_batch` returns when the batch completes, fails, is cancelled, or pauses for attention. Check `batch.paused_reason`. `get_batch(id, include_rows=False)` skips per-row data on large batches. `cancel_batch` stops new rows; rows already running finish. `retry_batch` re-runs failed rows and spends credits.
|
|
138
|
+
|
|
139
|
+
## Loops
|
|
140
|
+
|
|
141
|
+
A loop is a batch that repeats on a schedule. The first run starts immediately.
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
loop = agent.loop(
|
|
145
|
+
"Check acme.example for new filings",
|
|
146
|
+
{"frequency": "weekly", "schedule_config": {"hour": 9, "minute": 0, "day_of_week": 0}, "timezone": "America/New_York"},
|
|
147
|
+
name="Weekly filings",
|
|
148
|
+
max_credits=20,
|
|
149
|
+
)
|
|
150
|
+
history = agent.get_loop(loop.id)
|
|
151
|
+
agent.pause_loop(loop.id)
|
|
152
|
+
agent.delete_loop(loop.id, cancel_active=True)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`frequency` is `daily`, `weekly`, or `monthly`. Weekly uses `day_of_week` with 0 as Monday. Monthly uses `day_of_month` from 1 to 28. `max_credits` caps each occurrence, not the lifetime total. Inputs are frozen at creation; `update_loop` changes only the name and cadence.
|
|
156
|
+
|
|
157
|
+
## Campaigns
|
|
158
|
+
|
|
159
|
+
A campaign is a multi-stage plan. Save it as a draft, review it, then approve it with the digest of the plan you reviewed.
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
draft = agent.create_campaign(plan)
|
|
163
|
+
# read draft.stages and draft.deliverables
|
|
164
|
+
started = agent.approve_campaign(draft.id, draft.review_digest)
|
|
165
|
+
final = agent.wait_campaign(started.id)
|
|
166
|
+
artifact = agent.download_campaign_artifact(final.id, "companies")
|
|
167
|
+
open(artifact.filename or "companies.xlsx", "wb").write(artifact.content)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
An agent configured with `run_mode="campaign"` and a `campaign_plan` can start its template directly with `agent.campaign({"goal": "..."})`. That call starts work immediately. Approving with a stale digest raises `ConflictError`.
|
|
171
|
+
|
|
172
|
+
## Skills and integrations
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from agentrun import AgentRun
|
|
176
|
+
|
|
177
|
+
client = AgentRun()
|
|
178
|
+
skills = client.list_skills(query="company")
|
|
179
|
+
tools = client.list_integrations(query="company")
|
|
180
|
+
|
|
181
|
+
agent = client.agent(
|
|
182
|
+
name="Company verifier",
|
|
183
|
+
instructions="Verify the company and cite evidence.",
|
|
184
|
+
skills=[s.name for s in skills.skills],
|
|
185
|
+
tools=[t.name for t in tools.tools if not t.always_on],
|
|
186
|
+
)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Save your own Markdown as a skill with `client.create_skill(name, description, content)`. Creating a skill with an existing name updates it; the response has `created=False`.
|
|
190
|
+
|
|
191
|
+
## Errors
|
|
192
|
+
|
|
193
|
+
Every error inherits from `AgentRunError`.
|
|
194
|
+
|
|
195
|
+
| Error | When |
|
|
196
|
+
| --- | --- |
|
|
197
|
+
| `AuthenticationError` | 401. Bad or missing API key. |
|
|
198
|
+
| `PermissionDeniedError` | 403. The key lacks a scope or does not own the resource. |
|
|
199
|
+
| `NotFoundError` | 404. |
|
|
200
|
+
| `ConflictError` | 409. Stale review digest, or a loop already running. |
|
|
201
|
+
| `UnprocessableEntityError` | 422. `error.body["detail"]` lists the bad fields. |
|
|
202
|
+
| `RateLimitError` | 429. |
|
|
203
|
+
| `ServiceUnavailableError` | 503. Down, or the feature is not enabled for your account. |
|
|
204
|
+
| `APIConnectionError` | No HTTP response at all. `APITimeoutError` for a slow one. |
|
|
205
|
+
| `InvalidResponseError` | 2xx but not the JSON the SDK expects. Usually a wrong `base_url`. |
|
|
206
|
+
| `BuildError` | An agent build failed. |
|
|
207
|
+
| `PollTimeoutError` | A `wait*` call hit its local deadline. Also a `TimeoutError`. |
|
|
208
|
+
|
|
209
|
+
`APIError` subclasses carry `status_code`, `body`, and `request_id`. Include the request ID when you report a problem. The SDK checks the limits the API enforces before sending anything, such as skill content of at least 50 characters or `max_cost_usd` between 0.5 and 1000, and raises `ValueError` locally. The SDK never retries a request on its own. Pass the same `idempotency_key` to `run`, `batch`, `loop`, or `create_campaign` when you retry, so the server does not start the work twice.
|
|
210
|
+
|
|
211
|
+
## Configuration
|
|
212
|
+
|
|
213
|
+
| Setting | Argument | Environment variable | Default |
|
|
214
|
+
| --- | --- | --- | --- |
|
|
215
|
+
| API key | `api_key=` | `AGENTRUN_API_KEY` | required |
|
|
216
|
+
| API root | `base_url=` | `AGENTRUN_BASE_URL` | `https://api.grep.ai/api/v2` |
|
|
217
|
+
| Per-request timeout | `timeout=` | | 30 seconds |
|
|
218
|
+
|
|
219
|
+
`base_url` must use HTTPS. Plain HTTP is allowed only for localhost. Redirects are never followed. Use `with AgentRun() as client:` or `async with AsyncAgentRun() as client:` to close the connection pool.
|
|
220
|
+
|
|
221
|
+
## Types
|
|
222
|
+
|
|
223
|
+
The package ships `py.typed`. Responses are pydantic models, so `run.status` and `batch.rows` have the types your editor shows. Records keep any field the server adds later, reachable as an attribute or with `record["field"]`. `id` is always the record's ID, whatever the API calls it.
|
|
224
|
+
|
|
225
|
+
## Development
|
|
226
|
+
|
|
227
|
+
```sh
|
|
228
|
+
uv sync --group dev
|
|
229
|
+
uv run pytest
|
|
230
|
+
uv run ruff check src tests
|
|
231
|
+
uv run pyright
|
|
232
|
+
uv run mypy
|
|
233
|
+
uv build && uv run twine check dist/*
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Tests run against a fake API. Every test runs twice, once for the sync client and once for the async client.
|
|
237
|
+
|
|
238
|
+
Two tests tie the SDK to the backend:
|
|
239
|
+
|
|
240
|
+
- `tests/test_contract.py` validates every request the SDK sends, and every canned response, against `tests/fixtures/public-v2.json`, the backend's generated OpenAPI document. Refresh it with `python scripts/update_contract.py <path-or-url>` when the API changes.
|
|
241
|
+
- `tests/test_packaged_contract.py` starts a real local HTTP server and runs the same script the backend uses to verify packaged SDKs.
|
|
242
|
+
|
|
243
|
+
`scripts/smoke_staging.py` exercises a live deployment. It is read-only unless you pass `--run`.
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# AgentRun Python SDK
|
|
2
|
+
|
|
3
|
+
Create agents, run them on your inputs, and collect the results. Sync and async clients, typed responses, Python 3.10 or newer.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pip install parcha-agentrun
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The package on PyPI is `parcha-agentrun`. The module you import is `agentrun`.
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
Set `AGENTRUN_API_KEY` in your environment, then:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from agentrun import Agent
|
|
17
|
+
|
|
18
|
+
agent = Agent(
|
|
19
|
+
name="Company verifier",
|
|
20
|
+
instructions="Verify the company and cite primary sources.",
|
|
21
|
+
)
|
|
22
|
+
run = agent.run("Verify acme.example")
|
|
23
|
+
result = agent.wait(run.id)
|
|
24
|
+
|
|
25
|
+
print(result.status) # "completed", "failed", "cancelled", or "blocked"
|
|
26
|
+
print(result.markdown) # the report, or None
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`Agent` saves itself on the first call that needs it. Save `agent.id` and pass `Agent(agent_id=...)` next time to reuse it. Two things to know about `wait`. It returns for every terminal status, so check `result.status` before using the report. It gives up after 10 minutes by default and raises `PollTimeoutError`, but the run keeps going on the server. Call `agent.get_run(run.id)` later to fetch it.
|
|
30
|
+
|
|
31
|
+
## Build an agent from a description
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
agent = Agent.create("Research companies and cite sources.")
|
|
35
|
+
# Save agent.build_id if this process might stop.
|
|
36
|
+
agent.wait_until_ready() # builds can take a few minutes
|
|
37
|
+
run = agent.run("Research acme.example")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Resume an unfinished build with `Agent(build_id=...)`. Any run method waits for the build first. A build that fails raises `BuildError`; read `error.build.error` for the reason.
|
|
41
|
+
|
|
42
|
+
## Async
|
|
43
|
+
|
|
44
|
+
Every class has an async twin with the same methods. Works under asyncio and trio.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from agentrun import AsyncAgent
|
|
48
|
+
|
|
49
|
+
async def main() -> None:
|
|
50
|
+
agent = AsyncAgent(agent_id="agent-123")
|
|
51
|
+
run = await agent.run("Verify acme.example")
|
|
52
|
+
result = await agent.wait(run.id)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Statuses
|
|
56
|
+
|
|
57
|
+
The API reports progress in its own vocabulary. The SDK maps it to a short list so `is_done` and your code stay simple. The original value is always in `raw_status`.
|
|
58
|
+
|
|
59
|
+
| `run.status` | Means | Done |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| `queued` | Accepted, not started. | no |
|
|
62
|
+
| `moderation` | Content check in progress. | no |
|
|
63
|
+
| `running` | Working. | no |
|
|
64
|
+
| `blocked` | Needs your input, or content was rejected. | yes |
|
|
65
|
+
| `completed` | Report is ready. | yes |
|
|
66
|
+
| `failed` | Ran and failed, or never ran. | yes |
|
|
67
|
+
| `cancelled` | Cancelled, deleted, or superseded. | yes |
|
|
68
|
+
|
|
69
|
+
Batches use `queued`, `running`, `completed`, `completed_with_errors`, `failed`, and `cancelled`. Batch rows use `queued`, `running`, `completed`, and `failed`. A status the SDK does not recognise passes through unchanged.
|
|
70
|
+
|
|
71
|
+
## Structured output
|
|
72
|
+
|
|
73
|
+
Pass a pydantic model or a JSON schema dict as `output_schema`. The result's `structured_output` is a dict; `output_as` validates it into your model.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from pydantic import BaseModel
|
|
77
|
+
|
|
78
|
+
class Finding(BaseModel):
|
|
79
|
+
verified: bool
|
|
80
|
+
evidence: str
|
|
81
|
+
|
|
82
|
+
run = agent.run("Verify acme.example", output_schema=Finding)
|
|
83
|
+
finding = agent.wait(run.id).output_as(Finding)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Batches
|
|
87
|
+
|
|
88
|
+
Run up to 100 inputs at once. The SDK checks the limits before sending anything.
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
batch = agent.batch(
|
|
92
|
+
["Verify acme.example", "Verify beta.example"],
|
|
93
|
+
name="Q3 checks",
|
|
94
|
+
max_concurrent=5,
|
|
95
|
+
idempotency_key="q3-checks-001",
|
|
96
|
+
)
|
|
97
|
+
done = agent.wait_batch(batch.id)
|
|
98
|
+
for row in agent.get_batch_results(batch.id).results:
|
|
99
|
+
print(row.question, row.status, row.run_id)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
A CSV works too. Name the column that holds each input. The other columns come back as `row.row_data`.
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from agentrun import BatchCSV
|
|
106
|
+
|
|
107
|
+
batch = agent.batch(BatchCSV("companies.csv", input_column="website"))
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`BatchCSV` accepts a path, CSV text, bytes, or an open file. Files are limited to 1 MB.
|
|
111
|
+
|
|
112
|
+
`wait_batch` returns when the batch completes, fails, is cancelled, or pauses for attention. Check `batch.paused_reason`. `get_batch(id, include_rows=False)` skips per-row data on large batches. `cancel_batch` stops new rows; rows already running finish. `retry_batch` re-runs failed rows and spends credits.
|
|
113
|
+
|
|
114
|
+
## Loops
|
|
115
|
+
|
|
116
|
+
A loop is a batch that repeats on a schedule. The first run starts immediately.
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
loop = agent.loop(
|
|
120
|
+
"Check acme.example for new filings",
|
|
121
|
+
{"frequency": "weekly", "schedule_config": {"hour": 9, "minute": 0, "day_of_week": 0}, "timezone": "America/New_York"},
|
|
122
|
+
name="Weekly filings",
|
|
123
|
+
max_credits=20,
|
|
124
|
+
)
|
|
125
|
+
history = agent.get_loop(loop.id)
|
|
126
|
+
agent.pause_loop(loop.id)
|
|
127
|
+
agent.delete_loop(loop.id, cancel_active=True)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`frequency` is `daily`, `weekly`, or `monthly`. Weekly uses `day_of_week` with 0 as Monday. Monthly uses `day_of_month` from 1 to 28. `max_credits` caps each occurrence, not the lifetime total. Inputs are frozen at creation; `update_loop` changes only the name and cadence.
|
|
131
|
+
|
|
132
|
+
## Campaigns
|
|
133
|
+
|
|
134
|
+
A campaign is a multi-stage plan. Save it as a draft, review it, then approve it with the digest of the plan you reviewed.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
draft = agent.create_campaign(plan)
|
|
138
|
+
# read draft.stages and draft.deliverables
|
|
139
|
+
started = agent.approve_campaign(draft.id, draft.review_digest)
|
|
140
|
+
final = agent.wait_campaign(started.id)
|
|
141
|
+
artifact = agent.download_campaign_artifact(final.id, "companies")
|
|
142
|
+
open(artifact.filename or "companies.xlsx", "wb").write(artifact.content)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
An agent configured with `run_mode="campaign"` and a `campaign_plan` can start its template directly with `agent.campaign({"goal": "..."})`. That call starts work immediately. Approving with a stale digest raises `ConflictError`.
|
|
146
|
+
|
|
147
|
+
## Skills and integrations
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from agentrun import AgentRun
|
|
151
|
+
|
|
152
|
+
client = AgentRun()
|
|
153
|
+
skills = client.list_skills(query="company")
|
|
154
|
+
tools = client.list_integrations(query="company")
|
|
155
|
+
|
|
156
|
+
agent = client.agent(
|
|
157
|
+
name="Company verifier",
|
|
158
|
+
instructions="Verify the company and cite evidence.",
|
|
159
|
+
skills=[s.name for s in skills.skills],
|
|
160
|
+
tools=[t.name for t in tools.tools if not t.always_on],
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Save your own Markdown as a skill with `client.create_skill(name, description, content)`. Creating a skill with an existing name updates it; the response has `created=False`.
|
|
165
|
+
|
|
166
|
+
## Errors
|
|
167
|
+
|
|
168
|
+
Every error inherits from `AgentRunError`.
|
|
169
|
+
|
|
170
|
+
| Error | When |
|
|
171
|
+
| --- | --- |
|
|
172
|
+
| `AuthenticationError` | 401. Bad or missing API key. |
|
|
173
|
+
| `PermissionDeniedError` | 403. The key lacks a scope or does not own the resource. |
|
|
174
|
+
| `NotFoundError` | 404. |
|
|
175
|
+
| `ConflictError` | 409. Stale review digest, or a loop already running. |
|
|
176
|
+
| `UnprocessableEntityError` | 422. `error.body["detail"]` lists the bad fields. |
|
|
177
|
+
| `RateLimitError` | 429. |
|
|
178
|
+
| `ServiceUnavailableError` | 503. Down, or the feature is not enabled for your account. |
|
|
179
|
+
| `APIConnectionError` | No HTTP response at all. `APITimeoutError` for a slow one. |
|
|
180
|
+
| `InvalidResponseError` | 2xx but not the JSON the SDK expects. Usually a wrong `base_url`. |
|
|
181
|
+
| `BuildError` | An agent build failed. |
|
|
182
|
+
| `PollTimeoutError` | A `wait*` call hit its local deadline. Also a `TimeoutError`. |
|
|
183
|
+
|
|
184
|
+
`APIError` subclasses carry `status_code`, `body`, and `request_id`. Include the request ID when you report a problem. The SDK checks the limits the API enforces before sending anything, such as skill content of at least 50 characters or `max_cost_usd` between 0.5 and 1000, and raises `ValueError` locally. The SDK never retries a request on its own. Pass the same `idempotency_key` to `run`, `batch`, `loop`, or `create_campaign` when you retry, so the server does not start the work twice.
|
|
185
|
+
|
|
186
|
+
## Configuration
|
|
187
|
+
|
|
188
|
+
| Setting | Argument | Environment variable | Default |
|
|
189
|
+
| --- | --- | --- | --- |
|
|
190
|
+
| API key | `api_key=` | `AGENTRUN_API_KEY` | required |
|
|
191
|
+
| API root | `base_url=` | `AGENTRUN_BASE_URL` | `https://api.grep.ai/api/v2` |
|
|
192
|
+
| Per-request timeout | `timeout=` | | 30 seconds |
|
|
193
|
+
|
|
194
|
+
`base_url` must use HTTPS. Plain HTTP is allowed only for localhost. Redirects are never followed. Use `with AgentRun() as client:` or `async with AsyncAgentRun() as client:` to close the connection pool.
|
|
195
|
+
|
|
196
|
+
## Types
|
|
197
|
+
|
|
198
|
+
The package ships `py.typed`. Responses are pydantic models, so `run.status` and `batch.rows` have the types your editor shows. Records keep any field the server adds later, reachable as an attribute or with `record["field"]`. `id` is always the record's ID, whatever the API calls it.
|
|
199
|
+
|
|
200
|
+
## Development
|
|
201
|
+
|
|
202
|
+
```sh
|
|
203
|
+
uv sync --group dev
|
|
204
|
+
uv run pytest
|
|
205
|
+
uv run ruff check src tests
|
|
206
|
+
uv run pyright
|
|
207
|
+
uv run mypy
|
|
208
|
+
uv build && uv run twine check dist/*
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Tests run against a fake API. Every test runs twice, once for the sync client and once for the async client.
|
|
212
|
+
|
|
213
|
+
Two tests tie the SDK to the backend:
|
|
214
|
+
|
|
215
|
+
- `tests/test_contract.py` validates every request the SDK sends, and every canned response, against `tests/fixtures/public-v2.json`, the backend's generated OpenAPI document. Refresh it with `python scripts/update_contract.py <path-or-url>` when the API changes.
|
|
216
|
+
- `tests/test_packaged_contract.py` starts a real local HTTP server and runs the same script the backend uses to verify packaged SDKs.
|
|
217
|
+
|
|
218
|
+
`scripts/smoke_staging.py` exercises a live deployment. It is read-only unless you pass `--run`.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "parcha-agentrun"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Python SDK for AgentRun: create agents, run them, and collect results"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"httpx2>=2.12,<3",
|
|
13
|
+
"pydantic>=2.7,<3",
|
|
14
|
+
"anyio>=4.4,<5",
|
|
15
|
+
"typing-extensions>=4.12",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://agentrun.ai"
|
|
32
|
+
Documentation = "https://agentrun.ai/docs"
|
|
33
|
+
Repository = "https://github.com/Parcha-ai/agentrun-py"
|
|
34
|
+
Issues = "https://github.com/Parcha-ai/agentrun-py/issues"
|
|
35
|
+
|
|
36
|
+
[dependency-groups]
|
|
37
|
+
dev = [
|
|
38
|
+
"pytest>=8.3",
|
|
39
|
+
"pytest-cov>=6",
|
|
40
|
+
"mypy>=1.14",
|
|
41
|
+
"pyright>=1.1.400",
|
|
42
|
+
"ruff>=0.9",
|
|
43
|
+
"twine>=6",
|
|
44
|
+
"jsonschema>=4.23",
|
|
45
|
+
"types-jsonschema>=4.23",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/agentrun"]
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.sdist]
|
|
52
|
+
include = ["src/agentrun", "tests", "scripts", "README.md", "CHANGELOG.md", "pyproject.toml"]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
addopts = "-q"
|
|
57
|
+
|
|
58
|
+
[tool.ruff]
|
|
59
|
+
line-length = 110
|
|
60
|
+
target-version = "py310"
|
|
61
|
+
src = ["src", "tests"]
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint]
|
|
64
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF", "ANN", "T20"]
|
|
65
|
+
ignore = ["ANN401"]
|
|
66
|
+
|
|
67
|
+
[tool.ruff.lint.per-file-ignores]
|
|
68
|
+
"tests/*" = ["ANN", "T20"]
|
|
69
|
+
"scripts/*" = ["T20"]
|
|
70
|
+
|
|
71
|
+
[tool.pyright]
|
|
72
|
+
include = ["src", "tests"]
|
|
73
|
+
pythonVersion = "3.10"
|
|
74
|
+
typeCheckingMode = "strict"
|
|
75
|
+
reportMissingTypeStubs = false
|
|
76
|
+
|
|
77
|
+
[tool.mypy]
|
|
78
|
+
python_version = "3.10"
|
|
79
|
+
strict = true
|
|
80
|
+
files = ["src", "tests"]
|
|
81
|
+
plugins = ["pydantic.mypy"]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Exercise the SDK against a live deployment. Read-only unless you pass --run.
|
|
2
|
+
|
|
3
|
+
export AGENTRUN_API_KEY=...
|
|
4
|
+
export AGENTRUN_BASE_URL=https://staging-api.grep.ai/api/v2
|
|
5
|
+
python scripts/smoke_staging.py [--run]
|
|
6
|
+
|
|
7
|
+
Never prints the key. ``--run`` starts one low-effort run on the first agent
|
|
8
|
+
you own and waits for it, which spends credits.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from agentrun import Agent, AgentRun, AgentRunError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main(start_run: bool) -> int:
|
|
20
|
+
base = os.environ.get("AGENTRUN_BASE_URL", "(default)")
|
|
21
|
+
print(f"base_url: {base}")
|
|
22
|
+
with AgentRun() as client:
|
|
23
|
+
usage = client.get_usage()
|
|
24
|
+
print(f"usage: {usage.job_count} jobs, {usage.total_credits_used} credits in period")
|
|
25
|
+
skills = client.list_skills(limit=5)
|
|
26
|
+
print(f"skills: {skills.total} total, first: {[s.name for s in skills.skills]}")
|
|
27
|
+
tools = client.list_integrations(limit=5)
|
|
28
|
+
print(f"integrations: {tools.total} total, servers: {tools.servers[:5]}")
|
|
29
|
+
agents = client.list_agents()
|
|
30
|
+
print(f"agents you own: {len(agents)}")
|
|
31
|
+
page = client.list_runs(limit=3)
|
|
32
|
+
print(f"recent runs: {[(r.id, r.status, r.raw_status) for r in page.items]}")
|
|
33
|
+
if page.items:
|
|
34
|
+
run = client.get_run(page.items[0].id)
|
|
35
|
+
has_report = bool(run.markdown)
|
|
36
|
+
print(
|
|
37
|
+
f"run detail: status={run.status} raw={run.raw_status} done={run.is_done} report={has_report}"
|
|
38
|
+
)
|
|
39
|
+
batches = client.list_batches(limit=3)
|
|
40
|
+
print(f"recent batches: {[(b.id, b.status, b.raw_status) for b in batches.items]}")
|
|
41
|
+
if not start_run:
|
|
42
|
+
print("skipping run (pass --run to start one)")
|
|
43
|
+
return 0
|
|
44
|
+
if not agents:
|
|
45
|
+
print("no agent to run; create one first")
|
|
46
|
+
return 1
|
|
47
|
+
agent = Agent(client, agent_id=agents[0].id)
|
|
48
|
+
run = agent.run("Say hello and stop.", effort="low", idempotency_key="sdk-smoke-hello")
|
|
49
|
+
print(f"started run {run.id} status={run.status}")
|
|
50
|
+
result = agent.wait(run.id, timeout=600, interval=3)
|
|
51
|
+
print(f"finished run {result.id} status={result.status} raw={result.raw_status}")
|
|
52
|
+
print((result.markdown or "")[:300])
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
try:
|
|
58
|
+
sys.exit(main("--run" in sys.argv[1:]))
|
|
59
|
+
except AgentRunError as error:
|
|
60
|
+
print(f"failed: {type(error).__name__}: {error}")
|
|
61
|
+
sys.exit(1)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Refresh tests/fixtures/public-v2.json from the backend's generated OpenAPI document.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
python scripts/update_contract.py path/to/public_openapi.json
|
|
5
|
+
python scripts/update_contract.py https://api.grep.ai/api/v2/openapi.json
|
|
6
|
+
|
|
7
|
+
Only the canonical v2 resource paths the SDK calls are kept. Components are
|
|
8
|
+
kept whole so every $ref resolves.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
import urllib.request
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
KEEP = re.compile(r"^/api/v2/(agents|runs|loops|skills|integrations|batches|campaigns|usage)(/|$)")
|
|
20
|
+
TARGET = Path(__file__).resolve().parents[1] / "tests" / "fixtures" / "public-v2.json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main(source: str) -> None:
|
|
24
|
+
if source.startswith("http"):
|
|
25
|
+
with urllib.request.urlopen(source, timeout=30) as response:
|
|
26
|
+
document = json.load(response)
|
|
27
|
+
else:
|
|
28
|
+
document = json.loads(Path(source).read_text())
|
|
29
|
+
paths = {p: v for p, v in document["paths"].items() if KEEP.match(p)}
|
|
30
|
+
out = {
|
|
31
|
+
"openapi": document.get("openapi"),
|
|
32
|
+
"info": document.get("info"),
|
|
33
|
+
"paths": paths,
|
|
34
|
+
"components": document["components"],
|
|
35
|
+
}
|
|
36
|
+
TARGET.write_text(json.dumps(out, indent=1, sort_keys=True) + "\n")
|
|
37
|
+
print(f"wrote {TARGET.relative_to(Path.cwd())} with {len(paths)} paths")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if __name__ == "__main__":
|
|
41
|
+
if len(sys.argv) != 2:
|
|
42
|
+
sys.exit(__doc__)
|
|
43
|
+
main(sys.argv[1])
|