predev-api 0.12.0__tar.gz → 1.1.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: predev-api
3
- Version: 0.12.0
3
+ Version: 1.1.0
4
4
  Summary: Python client for the Pre.dev Architect API - Generate comprehensive software specifications
5
5
  Home-page: https://github.com/predotdev/predev-api
6
6
  Author: Pre.dev
@@ -502,7 +502,7 @@ For issues, questions, or contributions, please visit the [GitHub repository](ht
502
502
 
503
503
  ## Browser Tasks
504
504
 
505
- Run browser automation — scrape data, fill forms, navigate pages, extract structured data. Each task navigates a URL, optionally performs actions, and returns typed JSON.
505
+ Run browser automation — navigate, interact, and/or extract data from any web page. Each task navigates a URL, performs actions, and optionally returns typed JSON.
506
506
 
507
507
  ### Quick start
508
508
 
@@ -511,7 +511,7 @@ from predev_api import PredevAPI
511
511
 
512
512
  client = PredevAPI(api_key="your_api_key")
513
513
 
514
- result = client.browser_tasks([
514
+ result = client.browser_agent([
515
515
  {
516
516
  "url": "https://news.ycombinator.com",
517
517
  "instruction": "Extract the top 5 stories",
@@ -542,7 +542,7 @@ for story in result["results"][0]["data"]["stories"]:
542
542
  #### 1. Sync (default) — wait for completion
543
543
 
544
544
  ```python
545
- result = client.browser_tasks([
545
+ result = client.browser_agent([
546
546
  {"url": "https://example.com", "output": {"type": "object", "properties": {"heading": {"type": "string"}}}}
547
547
  ])
548
548
  print(result["results"][0]["data"]) # {'heading': 'Example Domain'}
@@ -554,7 +554,7 @@ print(result["totalCreditsUsed"]) # 0.1
554
554
  Yields events as the agent runs. Good for showing progress in a UI.
555
555
 
556
556
  ```python
557
- for msg in client.browser_tasks(tasks, stream=True):
557
+ for msg in client.browser_agent(tasks, stream=True):
558
558
  e, d = msg["event"], msg["data"]
559
559
  if e == "task_event":
560
560
  # navigation | screenshot | plan | action | validation | done
@@ -574,11 +574,11 @@ Returns the batch ID immediately. Use for long-running batches or background job
574
574
  ```python
575
575
  import time
576
576
 
577
- r = client.browser_tasks(tasks, run_async=True)
577
+ r = client.browser_agent(tasks, run_async=True)
578
578
  # {"id": "batch_abc", "status": "processing", "completed": 0, "total": 3}
579
579
 
580
580
  while True:
581
- state = client.get_browser_tasks(r["id"])
581
+ state = client.get_browser_agent(r["id"])
582
582
  print(f"{state['completed']}/{state['total']}")
583
583
  for done in state["results"]:
584
584
  print(f" ✓ {done['url']} -> {done.get('data')}")
@@ -600,27 +600,27 @@ Each task's behavior is determined by which fields are set:
600
600
 
601
601
  ### Retrieving a batch with the full timeline
602
602
 
603
- Every task records navigation, screenshots, LLM plans, actions, validations. Retrieve for audit, replay, or debugging:
603
+ Every task records navigation, screenshots, LLM plans, actions, validations. Retrieve for audit, replay, or debugging. Screenshots are uploaded to a CDN during execution — retrieved events contain `data["url"]`, not base64.
604
604
 
605
605
  ```python
606
- import base64
607
-
608
- details = client.get_browser_tasks(batch_id, include_events=True)
606
+ details = client.get_browser_agent(batch_id, include_events=True)
609
607
  for result in details["results"]:
610
608
  for ev in result.get("events", []):
611
609
  if ev["type"] == "screenshot":
612
- with open(f"screenshot_iter{ev.get('iteration')}.jpg", "wb") as f:
613
- f.write(base64.b64decode(ev["data"]["base64"]))
610
+ # ev["data"]["url"] is a permanent CDN URL. Use directly in an <img>.
611
+ print(f"Iter {ev.get('iteration')} screenshot:", ev["data"]["url"])
614
612
  elif ev["type"] == "plan":
615
613
  print(f"Iter {ev.get('iteration')} plan: {ev['data'].get('notes')}")
616
614
  ```
617
615
 
616
+ > **Note:** The live SSE stream (`stream=True`) still sends screenshots inline as base64 (`ev["data"]["base64"]`) so live UIs render instantly. The retrieval path (`get_browser_agent`) always returns CDN URLs.
617
+
618
618
  ### Parallel batch example
619
619
 
620
620
  ```python
621
621
  # Scrape 100 URLs with 20 browsers in parallel
622
622
  urls = [...100 urls...]
623
- result = client.browser_tasks(
623
+ result = client.browser_agent(
624
624
  [{"url": u, "output": {"type": "object", "properties": {"title": {"type": "string"}}}} for u in urls],
625
625
  concurrency=20
626
626
  )
@@ -631,10 +631,10 @@ print(f"{result['completed']}/{result['total']} done in {result['totalCreditsUse
631
631
 
632
632
  | Method | Returns | Use when |
633
633
  |---|---|---|
634
- | `browser_tasks(tasks, concurrency=N)` | dict | Default — wait for completion |
635
- | `browser_tasks(tasks, stream=True)` | iterator of SSE dicts | Live UI showing execution timeline |
636
- | `browser_tasks(tasks, run_async=True)` | dict (empty results, returned immediately) | Long batches, background jobs |
637
- | `get_browser_tasks(batch_id, include_events=False)` | dict | Poll progress or retrieve a completed batch |
634
+ | `browser_agent(tasks, concurrency=N)` | dict | Default — wait for completion |
635
+ | `browser_agent(tasks, stream=True)` | iterator of SSE dicts | Live UI showing execution timeline |
636
+ | `browser_agent(tasks, run_async=True)` | dict (empty results, returned immediately) | Long batches, background jobs |
637
+ | `get_browser_agent(batch_id, include_events=False)` | dict | Poll progress or retrieve a completed batch |
638
638
 
639
639
  ### Task result statuses
640
640
 
@@ -653,3 +653,51 @@ print(f"{result['completed']}/{result['total']} done in {result['totalCreditsUse
653
653
  - Minimum: **0.1 credits per task** ($0.01)
654
654
  - 10x margin on underlying LLM + sandbox compute
655
655
  - 1 credit = $0.10
656
+
657
+ ### Error handling
658
+
659
+ The browser-task endpoints raise typed exceptions for the most common
660
+ gating cases. Every billing-gate exception carries an `action_url` — a
661
+ deep link back to pre.dev that auto-opens the right modal (subscribe /
662
+ buy credits) when the user lands there:
663
+
664
+ ```python
665
+ from predev_api import (
666
+ PredevAPI,
667
+ InsufficientCreditsError,
668
+ SubscriptionRequiredError,
669
+ RateLimitError,
670
+ QueueFullError,
671
+ )
672
+ import webbrowser
673
+
674
+ client = PredevAPI(api_key="your_pre.dev_api_key")
675
+
676
+ try:
677
+ client.browser_agent([{"url": "https://example.com", "instruction": "extract h1"}])
678
+ except InsufficientCreditsError as e:
679
+ # Send the user to the credits modal — `action_url` is the canonical link.
680
+ if e.action_url:
681
+ webbrowser.open(e.action_url)
682
+ except SubscriptionRequiredError as e:
683
+ if e.action_url:
684
+ webbrowser.open(e.action_url)
685
+ except RateLimitError:
686
+ pass # back off and retry
687
+ except QueueFullError:
688
+ pass # wait for in-flight tasks to drain
689
+ ```
690
+
691
+ Mid-stream errors on the SSE stream raise the same typed exceptions, so a
692
+ `for msg in client.browser_agent(..., stream=True):` loop wrapped in
693
+ `try/except` is enough — no special event handling required.
694
+
695
+ | Exception | HTTP | `code` |
696
+ |---|---|---|
697
+ | `SubscriptionRequiredError` | 402 | `SUBSCRIPTION_REQUIRED` |
698
+ | `InsufficientCreditsError` | 402 | `INSUFFICIENT_CREDITS` |
699
+ | `RateLimitError` | 429 | `RATE_LIMITED` |
700
+ | `QueueFullError` | 429 | `QUEUE_FULL` |
701
+ | `BatchTooLargeError` | 400 | `BATCH_TOO_LARGE` |
702
+ | `AuthenticationError` | 401 | — |
703
+ | `PredevAPIError` | other | — |
@@ -462,7 +462,7 @@ For issues, questions, or contributions, please visit the [GitHub repository](ht
462
462
 
463
463
  ## Browser Tasks
464
464
 
465
- Run browser automation — scrape data, fill forms, navigate pages, extract structured data. Each task navigates a URL, optionally performs actions, and returns typed JSON.
465
+ Run browser automation — navigate, interact, and/or extract data from any web page. Each task navigates a URL, performs actions, and optionally returns typed JSON.
466
466
 
467
467
  ### Quick start
468
468
 
@@ -471,7 +471,7 @@ from predev_api import PredevAPI
471
471
 
472
472
  client = PredevAPI(api_key="your_api_key")
473
473
 
474
- result = client.browser_tasks([
474
+ result = client.browser_agent([
475
475
  {
476
476
  "url": "https://news.ycombinator.com",
477
477
  "instruction": "Extract the top 5 stories",
@@ -502,7 +502,7 @@ for story in result["results"][0]["data"]["stories"]:
502
502
  #### 1. Sync (default) — wait for completion
503
503
 
504
504
  ```python
505
- result = client.browser_tasks([
505
+ result = client.browser_agent([
506
506
  {"url": "https://example.com", "output": {"type": "object", "properties": {"heading": {"type": "string"}}}}
507
507
  ])
508
508
  print(result["results"][0]["data"]) # {'heading': 'Example Domain'}
@@ -514,7 +514,7 @@ print(result["totalCreditsUsed"]) # 0.1
514
514
  Yields events as the agent runs. Good for showing progress in a UI.
515
515
 
516
516
  ```python
517
- for msg in client.browser_tasks(tasks, stream=True):
517
+ for msg in client.browser_agent(tasks, stream=True):
518
518
  e, d = msg["event"], msg["data"]
519
519
  if e == "task_event":
520
520
  # navigation | screenshot | plan | action | validation | done
@@ -534,11 +534,11 @@ Returns the batch ID immediately. Use for long-running batches or background job
534
534
  ```python
535
535
  import time
536
536
 
537
- r = client.browser_tasks(tasks, run_async=True)
537
+ r = client.browser_agent(tasks, run_async=True)
538
538
  # {"id": "batch_abc", "status": "processing", "completed": 0, "total": 3}
539
539
 
540
540
  while True:
541
- state = client.get_browser_tasks(r["id"])
541
+ state = client.get_browser_agent(r["id"])
542
542
  print(f"{state['completed']}/{state['total']}")
543
543
  for done in state["results"]:
544
544
  print(f" ✓ {done['url']} -> {done.get('data')}")
@@ -560,27 +560,27 @@ Each task's behavior is determined by which fields are set:
560
560
 
561
561
  ### Retrieving a batch with the full timeline
562
562
 
563
- Every task records navigation, screenshots, LLM plans, actions, validations. Retrieve for audit, replay, or debugging:
563
+ Every task records navigation, screenshots, LLM plans, actions, validations. Retrieve for audit, replay, or debugging. Screenshots are uploaded to a CDN during execution — retrieved events contain `data["url"]`, not base64.
564
564
 
565
565
  ```python
566
- import base64
567
-
568
- details = client.get_browser_tasks(batch_id, include_events=True)
566
+ details = client.get_browser_agent(batch_id, include_events=True)
569
567
  for result in details["results"]:
570
568
  for ev in result.get("events", []):
571
569
  if ev["type"] == "screenshot":
572
- with open(f"screenshot_iter{ev.get('iteration')}.jpg", "wb") as f:
573
- f.write(base64.b64decode(ev["data"]["base64"]))
570
+ # ev["data"]["url"] is a permanent CDN URL. Use directly in an <img>.
571
+ print(f"Iter {ev.get('iteration')} screenshot:", ev["data"]["url"])
574
572
  elif ev["type"] == "plan":
575
573
  print(f"Iter {ev.get('iteration')} plan: {ev['data'].get('notes')}")
576
574
  ```
577
575
 
576
+ > **Note:** The live SSE stream (`stream=True`) still sends screenshots inline as base64 (`ev["data"]["base64"]`) so live UIs render instantly. The retrieval path (`get_browser_agent`) always returns CDN URLs.
577
+
578
578
  ### Parallel batch example
579
579
 
580
580
  ```python
581
581
  # Scrape 100 URLs with 20 browsers in parallel
582
582
  urls = [...100 urls...]
583
- result = client.browser_tasks(
583
+ result = client.browser_agent(
584
584
  [{"url": u, "output": {"type": "object", "properties": {"title": {"type": "string"}}}} for u in urls],
585
585
  concurrency=20
586
586
  )
@@ -591,10 +591,10 @@ print(f"{result['completed']}/{result['total']} done in {result['totalCreditsUse
591
591
 
592
592
  | Method | Returns | Use when |
593
593
  |---|---|---|
594
- | `browser_tasks(tasks, concurrency=N)` | dict | Default — wait for completion |
595
- | `browser_tasks(tasks, stream=True)` | iterator of SSE dicts | Live UI showing execution timeline |
596
- | `browser_tasks(tasks, run_async=True)` | dict (empty results, returned immediately) | Long batches, background jobs |
597
- | `get_browser_tasks(batch_id, include_events=False)` | dict | Poll progress or retrieve a completed batch |
594
+ | `browser_agent(tasks, concurrency=N)` | dict | Default — wait for completion |
595
+ | `browser_agent(tasks, stream=True)` | iterator of SSE dicts | Live UI showing execution timeline |
596
+ | `browser_agent(tasks, run_async=True)` | dict (empty results, returned immediately) | Long batches, background jobs |
597
+ | `get_browser_agent(batch_id, include_events=False)` | dict | Poll progress or retrieve a completed batch |
598
598
 
599
599
  ### Task result statuses
600
600
 
@@ -613,3 +613,51 @@ print(f"{result['completed']}/{result['total']} done in {result['totalCreditsUse
613
613
  - Minimum: **0.1 credits per task** ($0.01)
614
614
  - 10x margin on underlying LLM + sandbox compute
615
615
  - 1 credit = $0.10
616
+
617
+ ### Error handling
618
+
619
+ The browser-task endpoints raise typed exceptions for the most common
620
+ gating cases. Every billing-gate exception carries an `action_url` — a
621
+ deep link back to pre.dev that auto-opens the right modal (subscribe /
622
+ buy credits) when the user lands there:
623
+
624
+ ```python
625
+ from predev_api import (
626
+ PredevAPI,
627
+ InsufficientCreditsError,
628
+ SubscriptionRequiredError,
629
+ RateLimitError,
630
+ QueueFullError,
631
+ )
632
+ import webbrowser
633
+
634
+ client = PredevAPI(api_key="your_pre.dev_api_key")
635
+
636
+ try:
637
+ client.browser_agent([{"url": "https://example.com", "instruction": "extract h1"}])
638
+ except InsufficientCreditsError as e:
639
+ # Send the user to the credits modal — `action_url` is the canonical link.
640
+ if e.action_url:
641
+ webbrowser.open(e.action_url)
642
+ except SubscriptionRequiredError as e:
643
+ if e.action_url:
644
+ webbrowser.open(e.action_url)
645
+ except RateLimitError:
646
+ pass # back off and retry
647
+ except QueueFullError:
648
+ pass # wait for in-flight tasks to drain
649
+ ```
650
+
651
+ Mid-stream errors on the SSE stream raise the same typed exceptions, so a
652
+ `for msg in client.browser_agent(..., stream=True):` loop wrapped in
653
+ `try/except` is enough — no special event handling required.
654
+
655
+ | Exception | HTTP | `code` |
656
+ |---|---|---|
657
+ | `SubscriptionRequiredError` | 402 | `SUBSCRIPTION_REQUIRED` |
658
+ | `InsufficientCreditsError` | 402 | `INSUFFICIENT_CREDITS` |
659
+ | `RateLimitError` | 429 | `RATE_LIMITED` |
660
+ | `QueueFullError` | 429 | `QUEUE_FULL` |
661
+ | `BatchTooLargeError` | 400 | `BATCH_TOO_LARGE` |
662
+ | `AuthenticationError` | 401 | — |
663
+ | `PredevAPIError` | other | — |
@@ -27,14 +27,28 @@ from .client import (
27
27
  AlternativeTechStackItemApi,
28
28
  SpecEnrichedTechStackItem,
29
29
  )
30
- from .exceptions import PredevAPIError, AuthenticationError, RateLimitError
30
+ from .exceptions import (
31
+ PredevAPIError,
32
+ AuthenticationError,
33
+ RateLimitError,
34
+ SubscriptionRequiredError,
35
+ InsufficientCreditsError,
36
+ QueueFullError,
37
+ BatchTooLargeError,
38
+ exception_for_code,
39
+ )
31
40
 
32
- __version__ = "0.11.1"
41
+ __version__ = "1.1.0"
33
42
  __all__ = [
34
43
  "PredevAPI",
35
44
  "PredevAPIError",
36
45
  "AuthenticationError",
37
46
  "RateLimitError",
47
+ "SubscriptionRequiredError",
48
+ "InsufficientCreditsError",
49
+ "QueueFullError",
50
+ "BatchTooLargeError",
51
+ "exception_for_code",
38
52
  "ZippedDocsUrl",
39
53
  "SpecCoreFunctionality",
40
54
  "SpecTechStackItem",
@@ -6,7 +6,16 @@ from typing import Optional, Dict, Any, Literal, List, Union, BinaryIO, Iterator
6
6
  from dataclasses import dataclass
7
7
  import json
8
8
  import requests
9
- from .exceptions import PredevAPIError, AuthenticationError, RateLimitError
9
+ from .exceptions import (
10
+ PredevAPIError,
11
+ AuthenticationError,
12
+ RateLimitError,
13
+ SubscriptionRequiredError,
14
+ InsufficientCreditsError,
15
+ QueueFullError,
16
+ BatchTooLargeError,
17
+ exception_for_code,
18
+ )
10
19
 
11
20
 
12
21
  @dataclass
@@ -132,7 +141,9 @@ class SpecGraphNode:
132
141
  label: str = ""
133
142
  type: Optional[str] = None
134
143
  description: Optional[str] = None
135
- level: Optional[int] = None
144
+ # userFlowGraph: BFS depth from roles (roles = 0, flows >= 1, None when
145
+ # unreachable). architectureGraph: C4 diagram level ("C1" | "C2").
146
+ level: Optional[Union[int, str]] = None
136
147
  hours: Optional[float] = None
137
148
 
138
149
 
@@ -608,27 +619,27 @@ class PredevAPI:
608
619
  except requests.RequestException as e:
609
620
  raise PredevAPIError(f"Request failed: {str(e)}") from e
610
621
 
611
- def get_browser_tasks(
622
+ def get_browser_agent(
612
623
  self,
613
624
  batch_id: str,
614
625
  include_events: bool = False
615
626
  ) -> Dict[str, Any]:
616
627
  """
617
- Get the status + results of a browser tasks batch by ID.
628
+ Get the status + results of a browser-agent batch by ID.
618
629
 
619
- Works for both in-progress and completed batches - use with async=True
630
+ Works for both in-progress and completed batches - use with run_async=True
620
631
  submissions to poll for progress. Set include_events=True to get the
621
632
  full timeline (screenshots, plans, actions, validations) for each task.
622
633
 
623
634
  Args:
624
- batch_id: The batch ID returned from browser_tasks
635
+ batch_id: The batch ID returned from browser_agent()
625
636
  include_events: Include full event timeline (can be large due to screenshots)
626
637
 
627
638
  Returns:
628
- Dict with id, total, completed, results, totalCost, totalCreditsUsed, status
639
+ Dict with id, total, completed, results, totalCreditsUsed, status
629
640
  """
630
641
  qs = "?includeEvents=true" if include_events else ""
631
- url = f"{self.base_url}/api/v1/browser-tasks/{batch_id}{qs}"
642
+ url = f"{self.base_url}/browser-agent/{batch_id}{qs}"
632
643
  try:
633
644
  response = requests.get(url, headers=self.headers, timeout=60)
634
645
  self._handle_response(response)
@@ -636,7 +647,55 @@ class PredevAPI:
636
647
  except requests.RequestException as e:
637
648
  raise PredevAPIError(f"Request failed: {str(e)}") from e
638
649
 
639
- def browser_tasks(
650
+ def list_browser_agents(
651
+ self,
652
+ limit: Optional[int] = None,
653
+ skip: Optional[int] = None,
654
+ status: Optional[str] = None
655
+ ) -> Dict[str, Any]:
656
+ """
657
+ List browser-agent runs for the authenticated caller. Sorted by most recent.
658
+
659
+ Args:
660
+ limit: Page size (1-100, default 20)
661
+ skip: Offset for pagination
662
+ status: Optional filter ('processing' | 'completed')
663
+
664
+ Returns:
665
+ Dict with keys: batches, total, hasMore
666
+ """
667
+ params: Dict[str, Any] = {}
668
+ if limit is not None:
669
+ params["limit"] = limit
670
+ if skip is not None:
671
+ params["skip"] = skip
672
+ if status is not None:
673
+ params["status"] = status
674
+ url = f"{self.base_url}/list-browser-agents"
675
+ try:
676
+ response = requests.get(url, headers=self.headers, params=params, timeout=60)
677
+ self._handle_response(response)
678
+ return response.json()
679
+ except requests.RequestException as e:
680
+ raise PredevAPIError(f"Request failed: {str(e)}") from e
681
+
682
+ def browser_agent_status(self) -> Dict[str, Any]:
683
+ """
684
+ Caller's live queue snapshot. Useful for watching in-flight work
685
+ against the per-user queue cap (1000 live tasks).
686
+
687
+ Returns:
688
+ Dict with keys: userId, running, claimed, pending, total, cap
689
+ """
690
+ url = f"{self.base_url}/browser-agent-status"
691
+ try:
692
+ response = requests.get(url, headers=self.headers, timeout=30)
693
+ self._handle_response(response)
694
+ return response.json()
695
+ except requests.RequestException as e:
696
+ raise PredevAPIError(f"Request failed: {str(e)}") from e
697
+
698
+ def browser_agent(
640
699
  self,
641
700
  tasks: List[Dict[str, Any]],
642
701
  concurrency: Optional[int] = None,
@@ -644,7 +703,7 @@ class PredevAPI:
644
703
  run_async: bool = False
645
704
  ) -> Union[Dict[str, Any], Iterator[Dict[str, Any]]]:
646
705
  """
647
- Run browser automation tasks - scrape data, fill forms, navigate pages.
706
+ Run browser-agent automation tasks - scrape data, fill forms, navigate pages.
648
707
 
649
708
  Always pass an array of tasks (even for a single task). Each task has a
650
709
  url and optional instruction, input, and output schema.
@@ -658,9 +717,11 @@ class PredevAPI:
658
717
  concurrency: Parallel browsers (default 5, max 20)
659
718
  stream: If True, returns an iterator of SSE events instead of
660
719
  waiting for all tasks to complete
720
+ run_async: If True, returns immediately with the batch id; poll
721
+ get_browser_agent(id) for progress.
661
722
 
662
723
  Returns:
663
- Without stream: Dict with id, total, completed, results, totalCost, totalCreditsUsed
724
+ Without stream: Dict with id, total, completed, results, totalCreditsUsed
664
725
  With stream: Iterator yielding dicts with 'event' and 'data' keys:
665
726
  - event='task_event': real-time events (navigation, screenshot, plan, action, etc.)
666
727
  - event='task_result': a single task completed
@@ -669,20 +730,20 @@ class PredevAPI:
669
730
 
670
731
  Example:
671
732
  >>> # Standard mode
672
- >>> result = client.browser_tasks([
733
+ >>> result = client.browser_agent([
673
734
  ... {"url": "https://example.com", "output": {"type": "object", "properties": {"heading": {"type": "string"}}}}
674
735
  ... ])
675
736
  >>> print(result["results"][0]["data"])
676
737
  >>>
677
738
  >>> # Stream mode
678
- >>> for msg in client.browser_tasks([...], stream=True):
739
+ >>> for msg in client.browser_agent([...], stream=True):
679
740
  ... if msg["event"] == "done":
680
741
  ... print("Done:", msg["data"]["totalCreditsUsed"], "credits")
681
742
  """
682
743
  if stream:
683
- return self._browser_tasks_stream(tasks, concurrency)
744
+ return self._browser_agent_stream(tasks, concurrency)
684
745
 
685
- url = f"{self.base_url}/api/v1/browser-tasks"
746
+ url = f"{self.base_url}/browser-agent"
686
747
  payload: Dict[str, Any] = {"tasks": tasks}
687
748
  if concurrency is not None:
688
749
  payload["concurrency"] = concurrency
@@ -701,13 +762,13 @@ class PredevAPI:
701
762
  except requests.RequestException as e:
702
763
  raise PredevAPIError(f"Request failed: {str(e)}") from e
703
764
 
704
- def _browser_tasks_stream(
765
+ def _browser_agent_stream(
705
766
  self,
706
767
  tasks: List[Dict[str, Any]],
707
768
  concurrency: Optional[int] = None
708
769
  ) -> Iterator[Dict[str, Any]]:
709
- """SSE streaming implementation for browser_tasks."""
710
- url = f"{self.base_url}/api/v1/browser-tasks"
770
+ """SSE streaming implementation for browser_agent."""
771
+ url = f"{self.base_url}/browser-agent"
711
772
  payload: Dict[str, Any] = {"tasks": tasks, "stream": True}
712
773
  if concurrency is not None:
713
774
  payload["concurrency"] = concurrency
@@ -732,6 +793,18 @@ class PredevAPI:
732
793
  current_event = line[7:].strip()
733
794
  elif line.startswith("data: ") and current_event:
734
795
  data = json.loads(line[6:])
796
+ # Mid-stream `error` events carry the same
797
+ # `{ error, code, actionUrl? }` shape the REST endpoints
798
+ # do — surface them as typed exceptions so callers can
799
+ # `except InsufficientCreditsError` cleanly.
800
+ if current_event == "error" and isinstance(data, dict):
801
+ raise exception_for_code(
802
+ data.get("code"),
803
+ data.get("error")
804
+ or data.get("message")
805
+ or "Browser agent error",
806
+ data.get("actionUrl") or data.get("action_url"),
807
+ )
735
808
  yield {"event": current_event, "data": data}
736
809
  current_event = ""
737
810
  except requests.RequestException as e:
@@ -896,22 +969,45 @@ class PredevAPI:
896
969
  return {"file": (filename, file)}
897
970
 
898
971
  def _handle_response(self, response: requests.Response) -> None:
899
- """Handle API response and raise appropriate exceptions."""
972
+ """Handle API response and raise appropriate exceptions.
973
+
974
+ Browser-task gating errors come back as a structured body
975
+ ``{"error": ..., "code": ..., "actionUrl": ...}`` (HTTP 402 / 429 /
976
+ 400). We parse the body before short-circuiting on status code so
977
+ ``code: SUBSCRIPTION_REQUIRED`` becomes :class:`SubscriptionRequiredError`
978
+ with its ``action_url`` populated, instead of a generic
979
+ :class:`PredevAPIError`.
980
+ """
900
981
  if response.status_code == 200:
901
982
  return
902
983
 
984
+ # Try to parse the structured error body up front.
985
+ error_body = None
986
+ error_message = "Unknown error"
987
+ try:
988
+ error_body = response.json()
989
+ error_message = (
990
+ error_body.get("error")
991
+ or error_body.get("message")
992
+ or str(error_body)
993
+ )
994
+ except Exception:
995
+ error_message = response.text or f"HTTP {response.status_code}"
996
+
997
+ # Structured `code` wins — covers SUBSCRIPTION_REQUIRED /
998
+ # INSUFFICIENT_CREDITS / QUEUE_FULL / BATCH_TOO_LARGE / RATE_LIMITED.
999
+ if isinstance(error_body, dict) and error_body.get("code"):
1000
+ raise exception_for_code(
1001
+ error_body["code"],
1002
+ error_message,
1003
+ error_body.get("actionUrl") or error_body.get("action_url"),
1004
+ )
1005
+
903
1006
  if response.status_code == 401:
904
1007
  raise AuthenticationError("Invalid API key")
905
1008
 
906
1009
  if response.status_code == 429:
907
- raise RateLimitError("Rate limit exceeded")
908
-
909
- try:
910
- error_data = response.json()
911
- error_message = error_data.get("error") or error_data.get(
912
- "message") or str(error_data)
913
- except Exception:
914
- error_message = response.text or "Unknown error"
1010
+ raise RateLimitError(error_message)
915
1011
 
916
1012
  raise PredevAPIError(
917
1013
  f"API request failed with status {response.status_code}: {error_message}"
@@ -0,0 +1,86 @@
1
+ """
2
+ Custom exceptions for the Pre.dev API client.
3
+
4
+ Browser-task gating errors carry the same shape as the Node SDK:
5
+ ``code`` selects a typed exception, and ``action_url`` is a deep link
6
+ back to pre.dev that auto-opens the right billing modal (subscribe /
7
+ buy credits) when the user lands there. Older endpoints just return
8
+ ``{"error": "..."}`` and surface as ``PredevAPIError``.
9
+ """
10
+
11
+ from typing import Optional
12
+
13
+
14
+ class PredevAPIError(Exception):
15
+ """Base exception for Pre.dev API errors."""
16
+ pass
17
+
18
+
19
+ class AuthenticationError(PredevAPIError):
20
+ """Raised when authentication fails (HTTP 401)."""
21
+ pass
22
+
23
+
24
+ class RateLimitError(PredevAPIError):
25
+ """Raised when a request is rate-limited (HTTP 429, ``code: RATE_LIMITED``)."""
26
+ pass
27
+
28
+
29
+ class SubscriptionRequiredError(PredevAPIError):
30
+ """HTTP 402, ``code: SUBSCRIPTION_REQUIRED``.
31
+
32
+ Trial limit reached on this API key — the user needs an active
33
+ subscription to keep running browser-agent tasks.
34
+
35
+ ``action_url``, when present, is a deep link back to pre.dev that
36
+ auto-opens the subscribe modal.
37
+ """
38
+
39
+ def __init__(self, message: str, action_url: Optional[str] = None):
40
+ super().__init__(message)
41
+ self.action_url = action_url
42
+
43
+
44
+ class InsufficientCreditsError(PredevAPIError):
45
+ """HTTP 402, ``code: INSUFFICIENT_CREDITS``.
46
+
47
+ Subscription is fine but the credit balance is too low to cover this
48
+ request. ``action_url`` deep-links to the credit-purchase modal.
49
+ """
50
+
51
+ def __init__(self, message: str, action_url: Optional[str] = None):
52
+ super().__init__(message)
53
+ self.action_url = action_url
54
+
55
+
56
+ class QueueFullError(PredevAPIError):
57
+ """HTTP 429, ``code: QUEUE_FULL`` — too many in-flight tasks for this user."""
58
+ pass
59
+
60
+
61
+ class BatchTooLargeError(PredevAPIError):
62
+ """HTTP 400, ``code: BATCH_TOO_LARGE`` — request had more tasks than the per-batch maximum."""
63
+ pass
64
+
65
+
66
+ def exception_for_code(
67
+ code: Optional[str],
68
+ message: str,
69
+ action_url: Optional[str] = None,
70
+ ) -> PredevAPIError:
71
+ """Map a structured error ``code`` to the right typed exception.
72
+
73
+ Falls back to :class:`PredevAPIError` for unknown codes so callers
74
+ that just want ``str(e)`` keep working.
75
+ """
76
+ if code == "SUBSCRIPTION_REQUIRED":
77
+ return SubscriptionRequiredError(message, action_url)
78
+ if code == "INSUFFICIENT_CREDITS":
79
+ return InsufficientCreditsError(message, action_url)
80
+ if code == "QUEUE_FULL":
81
+ return QueueFullError(message)
82
+ if code == "BATCH_TOO_LARGE":
83
+ return BatchTooLargeError(message)
84
+ if code == "RATE_LIMITED":
85
+ return RateLimitError(message)
86
+ return PredevAPIError(message)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: predev-api
3
- Version: 0.12.0
3
+ Version: 1.1.0
4
4
  Summary: Python client for the Pre.dev Architect API - Generate comprehensive software specifications
5
5
  Home-page: https://github.com/predotdev/predev-api
6
6
  Author: Pre.dev
@@ -502,7 +502,7 @@ For issues, questions, or contributions, please visit the [GitHub repository](ht
502
502
 
503
503
  ## Browser Tasks
504
504
 
505
- Run browser automation — scrape data, fill forms, navigate pages, extract structured data. Each task navigates a URL, optionally performs actions, and returns typed JSON.
505
+ Run browser automation — navigate, interact, and/or extract data from any web page. Each task navigates a URL, performs actions, and optionally returns typed JSON.
506
506
 
507
507
  ### Quick start
508
508
 
@@ -511,7 +511,7 @@ from predev_api import PredevAPI
511
511
 
512
512
  client = PredevAPI(api_key="your_api_key")
513
513
 
514
- result = client.browser_tasks([
514
+ result = client.browser_agent([
515
515
  {
516
516
  "url": "https://news.ycombinator.com",
517
517
  "instruction": "Extract the top 5 stories",
@@ -542,7 +542,7 @@ for story in result["results"][0]["data"]["stories"]:
542
542
  #### 1. Sync (default) — wait for completion
543
543
 
544
544
  ```python
545
- result = client.browser_tasks([
545
+ result = client.browser_agent([
546
546
  {"url": "https://example.com", "output": {"type": "object", "properties": {"heading": {"type": "string"}}}}
547
547
  ])
548
548
  print(result["results"][0]["data"]) # {'heading': 'Example Domain'}
@@ -554,7 +554,7 @@ print(result["totalCreditsUsed"]) # 0.1
554
554
  Yields events as the agent runs. Good for showing progress in a UI.
555
555
 
556
556
  ```python
557
- for msg in client.browser_tasks(tasks, stream=True):
557
+ for msg in client.browser_agent(tasks, stream=True):
558
558
  e, d = msg["event"], msg["data"]
559
559
  if e == "task_event":
560
560
  # navigation | screenshot | plan | action | validation | done
@@ -574,11 +574,11 @@ Returns the batch ID immediately. Use for long-running batches or background job
574
574
  ```python
575
575
  import time
576
576
 
577
- r = client.browser_tasks(tasks, run_async=True)
577
+ r = client.browser_agent(tasks, run_async=True)
578
578
  # {"id": "batch_abc", "status": "processing", "completed": 0, "total": 3}
579
579
 
580
580
  while True:
581
- state = client.get_browser_tasks(r["id"])
581
+ state = client.get_browser_agent(r["id"])
582
582
  print(f"{state['completed']}/{state['total']}")
583
583
  for done in state["results"]:
584
584
  print(f" ✓ {done['url']} -> {done.get('data')}")
@@ -600,27 +600,27 @@ Each task's behavior is determined by which fields are set:
600
600
 
601
601
  ### Retrieving a batch with the full timeline
602
602
 
603
- Every task records navigation, screenshots, LLM plans, actions, validations. Retrieve for audit, replay, or debugging:
603
+ Every task records navigation, screenshots, LLM plans, actions, validations. Retrieve for audit, replay, or debugging. Screenshots are uploaded to a CDN during execution — retrieved events contain `data["url"]`, not base64.
604
604
 
605
605
  ```python
606
- import base64
607
-
608
- details = client.get_browser_tasks(batch_id, include_events=True)
606
+ details = client.get_browser_agent(batch_id, include_events=True)
609
607
  for result in details["results"]:
610
608
  for ev in result.get("events", []):
611
609
  if ev["type"] == "screenshot":
612
- with open(f"screenshot_iter{ev.get('iteration')}.jpg", "wb") as f:
613
- f.write(base64.b64decode(ev["data"]["base64"]))
610
+ # ev["data"]["url"] is a permanent CDN URL. Use directly in an <img>.
611
+ print(f"Iter {ev.get('iteration')} screenshot:", ev["data"]["url"])
614
612
  elif ev["type"] == "plan":
615
613
  print(f"Iter {ev.get('iteration')} plan: {ev['data'].get('notes')}")
616
614
  ```
617
615
 
616
+ > **Note:** The live SSE stream (`stream=True`) still sends screenshots inline as base64 (`ev["data"]["base64"]`) so live UIs render instantly. The retrieval path (`get_browser_agent`) always returns CDN URLs.
617
+
618
618
  ### Parallel batch example
619
619
 
620
620
  ```python
621
621
  # Scrape 100 URLs with 20 browsers in parallel
622
622
  urls = [...100 urls...]
623
- result = client.browser_tasks(
623
+ result = client.browser_agent(
624
624
  [{"url": u, "output": {"type": "object", "properties": {"title": {"type": "string"}}}} for u in urls],
625
625
  concurrency=20
626
626
  )
@@ -631,10 +631,10 @@ print(f"{result['completed']}/{result['total']} done in {result['totalCreditsUse
631
631
 
632
632
  | Method | Returns | Use when |
633
633
  |---|---|---|
634
- | `browser_tasks(tasks, concurrency=N)` | dict | Default — wait for completion |
635
- | `browser_tasks(tasks, stream=True)` | iterator of SSE dicts | Live UI showing execution timeline |
636
- | `browser_tasks(tasks, run_async=True)` | dict (empty results, returned immediately) | Long batches, background jobs |
637
- | `get_browser_tasks(batch_id, include_events=False)` | dict | Poll progress or retrieve a completed batch |
634
+ | `browser_agent(tasks, concurrency=N)` | dict | Default — wait for completion |
635
+ | `browser_agent(tasks, stream=True)` | iterator of SSE dicts | Live UI showing execution timeline |
636
+ | `browser_agent(tasks, run_async=True)` | dict (empty results, returned immediately) | Long batches, background jobs |
637
+ | `get_browser_agent(batch_id, include_events=False)` | dict | Poll progress or retrieve a completed batch |
638
638
 
639
639
  ### Task result statuses
640
640
 
@@ -653,3 +653,51 @@ print(f"{result['completed']}/{result['total']} done in {result['totalCreditsUse
653
653
  - Minimum: **0.1 credits per task** ($0.01)
654
654
  - 10x margin on underlying LLM + sandbox compute
655
655
  - 1 credit = $0.10
656
+
657
+ ### Error handling
658
+
659
+ The browser-task endpoints raise typed exceptions for the most common
660
+ gating cases. Every billing-gate exception carries an `action_url` — a
661
+ deep link back to pre.dev that auto-opens the right modal (subscribe /
662
+ buy credits) when the user lands there:
663
+
664
+ ```python
665
+ from predev_api import (
666
+ PredevAPI,
667
+ InsufficientCreditsError,
668
+ SubscriptionRequiredError,
669
+ RateLimitError,
670
+ QueueFullError,
671
+ )
672
+ import webbrowser
673
+
674
+ client = PredevAPI(api_key="your_pre.dev_api_key")
675
+
676
+ try:
677
+ client.browser_agent([{"url": "https://example.com", "instruction": "extract h1"}])
678
+ except InsufficientCreditsError as e:
679
+ # Send the user to the credits modal — `action_url` is the canonical link.
680
+ if e.action_url:
681
+ webbrowser.open(e.action_url)
682
+ except SubscriptionRequiredError as e:
683
+ if e.action_url:
684
+ webbrowser.open(e.action_url)
685
+ except RateLimitError:
686
+ pass # back off and retry
687
+ except QueueFullError:
688
+ pass # wait for in-flight tasks to drain
689
+ ```
690
+
691
+ Mid-stream errors on the SSE stream raise the same typed exceptions, so a
692
+ `for msg in client.browser_agent(..., stream=True):` loop wrapped in
693
+ `try/except` is enough — no special event handling required.
694
+
695
+ | Exception | HTTP | `code` |
696
+ |---|---|---|
697
+ | `SubscriptionRequiredError` | 402 | `SUBSCRIPTION_REQUIRED` |
698
+ | `InsufficientCreditsError` | 402 | `INSUFFICIENT_CREDITS` |
699
+ | `RateLimitError` | 429 | `RATE_LIMITED` |
700
+ | `QueueFullError` | 429 | `QUEUE_FULL` |
701
+ | `BatchTooLargeError` | 400 | `BATCH_TOO_LARGE` |
702
+ | `AuthenticationError` | 401 | — |
703
+ | `PredevAPIError` | other | — |
@@ -9,7 +9,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
9
9
 
10
10
  setup(
11
11
  name="predev-api",
12
- version="0.12.0",
12
+ version="1.1.0",
13
13
  author="Pre.dev",
14
14
  author_email="support@pre.dev",
15
15
  description="Python client for the Pre.dev Architect API - Generate comprehensive software specifications",
@@ -1,18 +0,0 @@
1
- """
2
- Custom exceptions for the Pre.dev API client
3
- """
4
-
5
-
6
- class PredevAPIError(Exception):
7
- """Base exception for Pre.dev API errors."""
8
- pass
9
-
10
-
11
- class AuthenticationError(PredevAPIError):
12
- """Raised when authentication fails."""
13
- pass
14
-
15
-
16
- class RateLimitError(PredevAPIError):
17
- """Raised when rate limit is exceeded."""
18
- pass
File without changes
File without changes
File without changes