fsanyoto commited on
Commit
5bfda9a
Β·
verified Β·
1 Parent(s): 8513024

Deploy AIOS web (React glide grid + FastAPI slice)

Browse files
Files changed (3) hide show
  1. RELEASES.json +1 -1
  2. VERSION +1 -1
  3. api/automation_engine.py +109 -5
RELEASES.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
- "current": "43c0c66",
3
  "releases": [
4
  {
5
  "version": "v14",
 
1
  {
2
+ "current": "b859a84",
3
  "releases": [
4
  {
5
  "version": "v14",
VERSION CHANGED
@@ -1 +1 @@
1
- 43c0c66
 
1
+ b859a84
api/automation_engine.py CHANGED
@@ -2695,6 +2695,76 @@ def bd_scrape(dataset_id, urls, wait=None):
2695
  f"or collect {sid} from the control panel")
2696
 
2697
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2698
  def pull_profile_bd(url, max_posts=DEFAULT_POSTS_PER_PULL, post_metrics=False, log=print):
2699
  """The paid rung: one profile, EXACT counts, plus the top posts the anonymous surface hides.
2700
 
@@ -2711,11 +2781,41 @@ def pull_profile_bd(url, max_posts=DEFAULT_POSTS_PER_PULL, post_metrics=False, l
2711
  return {"state": "error", "note": f"{url!r} is not an Instagram profile URL",
2712
  "profile": {}, "posts": [], "via": ""}
2713
  rows, note = bd_scrape(BD_DS_PROFILES, [f"https://www.instagram.com/{handle}/"])
2714
- if note:
2715
- return {"state": "blocked", "profile": {}, "posts": [], "via": "brightdata",
2716
- "note": note}
2717
  node = rows[0] if rows else {}
2718
- profile = _bd_profile(node, handle)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2719
  if profile.get("followers") is None and profile.get("following") is None:
2720
  # It answered 200 with something we could not read. Say THAT β€” not "0 followers".
2721
  # ⚠ `posts_count` is deliberately NOT part of this test: it arrives as a fabricated 0 and
@@ -2754,8 +2854,12 @@ def pull_profile_bd(url, max_posts=DEFAULT_POSTS_PER_PULL, post_metrics=False, l
2754
  # IDENTITY WITHOUT MEDIA IS STILL `partial`, on the paid rung too. The rule does not soften
2755
  # because we are paying: a run that wrote a follower count and no posts must not paint green
2756
  # over a posts table that did not grow.
 
 
 
 
2757
  return {"state": "ok" if posts else "partial", "profile": profile, "posts": posts,
2758
- "via": "brightdata",
2759
  "note": note or ("" if posts else "profile read; no posts were returned")}
2760
 
2761
 
 
2695
  f"or collect {sid} from the control panel")
2696
 
2697
 
2698
+ #: ⭐⭐ 2026-08-07 β€” HOW LONG THE CORPUS RUNG MAY WAIT. A corpus query is a scan over 620M rows
2699
+ #: and takes MINUTES, not seconds (Β§2d MEASURED ~4 min for a narrow one). It is a FALLBACK behind
2700
+ #: a scrape that already failed, so the budget is generous β€” but bounded, because an enrich step
2701
+ #: that never returns is an automation that never finishes.
2702
+ BD_CORPUS_WAIT = float(os.environ.get("AIOS_BD_CORPUS_WAIT") or 420)
2703
+ BD_CORPUS_POLL = float(os.environ.get("AIOS_BD_CORPUS_POLL") or 15)
2704
+
2705
+
2706
+ def bd_corpus_profile(handle, log=print):
2707
+ """One profile out of the PRE-COLLECTED corpus. `(node|None, note)`.
2708
+
2709
+ ⭐⭐ WHY THIS RUNG EXISTS, and it is measured rather than defensive. On 2026-08-07 the SCRAPE
2710
+ path answered `Parse error: Invalid URL` for `inayma` on **four consecutive attempts and
2711
+ through both input shapes** (a URL with and without its trailing slash, and the `user_name`
2712
+ discovery input, which the vendor converts to the same URL internally) β€” while a control
2713
+ profile in the SAME batch came back with 36 keys. So the URL form was not the problem and
2714
+ neither was the account: the anonymous ladder read it fine. The vendor simply could not
2715
+ scrape that profile.
2716
+ β‡’ **The same dataset answered instantly by the OTHER route.** `POST /datasets/filter` with
2717
+ `account = inayma` returned the full 33-key row β€” exact followers (203,818 against the
2718
+ anonymous rung's rounded 204,000), biography, `is_verified`, `avg_engagement`, `fbid`,
2719
+ `highlights_count`. One dataset, two routes, and only one of them was broken.
2720
+
2721
+ ⚠ THE ROWS ARE PRE-COLLECTED, so this is not a live read β€” it is the vendor's own last
2722
+ capture, and its population differs from the scrape path's (Β§2d: `avg_engagement` 88%
2723
+ populated on the corpus vs 0% on scrape). That is why it is a FALLBACK and not the default:
2724
+ fresher-but-thinner beats staler-but-fuller when both work.
2725
+ β›” THE SNAPSHOT NAMESPACES ARE NOT INTERCHANGEABLE. A corpus query mints `snap_…` and is read
2726
+ at `/datasets/snapshot/{id}`; the scraper mints `sd_…` and is read at
2727
+ `/datasets/v3/snapshot/{id}`. Crossing them returns a flat 404 about a snapshot that is alive
2728
+ and building β€” the trap Β§2c records, and the reason these paths are spelled out here.
2729
+ """
2730
+ h = str(handle or "").strip().lstrip("@").lower()
2731
+ if not h:
2732
+ return None, "no handle to look up"
2733
+ payload, note = bd_call(BD_PATH_FILTER, None,
2734
+ {"dataset_id": BD_DS_PROFILES, "records_limit": 1,
2735
+ "filter": {"name": "account", "operator": "=", "value": h}})
2736
+ if note:
2737
+ return None, note
2738
+ snap = str((payload or {}).get("snapshot_id") or "").strip()
2739
+ if not snap:
2740
+ return None, "the profile corpus accepted the query but named no snapshot"
2741
+ deadline = time.time() + BD_CORPUS_WAIT
2742
+ while time.time() < deadline:
2743
+ time.sleep(BD_CORPUS_POLL)
2744
+ got, perr = bd_call(f"{BD_PATH_FILTER_SNAPSHOT}/{snap}", None, None)
2745
+ status = str((got or {}).get("status") or "").strip()
2746
+ if perr or status in ("failed", "error"):
2747
+ return None, (perr or f"the corpus query failed ({(got or {}).get('error') or status})")
2748
+ if status == "ready":
2749
+ # ⚠ READY IS NOT DELIVERABLE. §2d MEASURED `/download` answering "Snapshot is
2750
+ # building. Try again in a few minutes" while status already said `ready` β€” delivery
2751
+ # lags readiness. So the download is polled too, inside the same budget.
2752
+ while time.time() < deadline:
2753
+ rows, derr = bd_call(f"{BD_PATH_FILTER_SNAPSHOT}/{snap}/download",
2754
+ {"format": "json"}, None)
2755
+ if derr:
2756
+ time.sleep(BD_CORPUS_POLL)
2757
+ continue
2758
+ if isinstance(rows, dict):
2759
+ rows = [rows]
2760
+ if isinstance(rows, list) and rows:
2761
+ return rows[0], ""
2762
+ time.sleep(BD_CORPUS_POLL)
2763
+ break
2764
+ return None, (f"the profile corpus did not deliver {h!r} within "
2765
+ f"{int(BD_CORPUS_WAIT)}s (snapshot {snap} is still building β€” it is not lost)")
2766
+
2767
+
2768
  def pull_profile_bd(url, max_posts=DEFAULT_POSTS_PER_PULL, post_metrics=False, log=print):
2769
  """The paid rung: one profile, EXACT counts, plus the top posts the anonymous surface hides.
2770
 
 
2781
  return {"state": "error", "note": f"{url!r} is not an Instagram profile URL",
2782
  "profile": {}, "posts": [], "via": ""}
2783
  rows, note = bd_scrape(BD_DS_PROFILES, [f"https://www.instagram.com/{handle}/"])
 
 
 
2784
  node = rows[0] if rows else {}
2785
+ profile = _bd_profile(node, handle) if node else {}
2786
+ unreadable = profile.get("followers") is None and profile.get("following") is None
2787
+ via = "brightdata"
2788
+ if note or unreadable:
2789
+ # ⭐⭐ 2026-08-07 β€” THE CORPUS IS TRIED BEFORE THIS RUNG GIVES UP, and it is the difference
2790
+ # between a blank row and a full one. MEASURED: the scraper answered `Parse error: Invalid
2791
+ # URL` for `inayma` four times running and through both input shapes, while a control
2792
+ # profile in the same batch returned 36 keys β€” and `POST /datasets/filter` on the SAME
2793
+ # dataset returned that profile's complete 33-key row. A rung that reports `blocked`
2794
+ # while the vendor is holding the answer on another route is a rung that gave up early.
2795
+ # ⚠ The FIRST failure is kept in the note either way: if the corpus also misses, the
2796
+ # person needs to know the scrape was tried and what it said, not only the fallback.
2797
+ # ⚠ THE UNREADABLE-ROW DIAGNOSTIC IS CARRIED, NOT DROPPED. "It answered 200 with something
2798
+ # we could not parse" and "it refused" are different problems with different fixes, and
2799
+ # the field-names hint is the one that tells the next reader to go and diff a real row
2800
+ # against the schema. Losing it behind the corpus fallback would make a mapping
2801
+ # regression look like a vendor outage.
2802
+ scrape_note = note or ("the scrape answered, but no follower/following counts were "
2803
+ "readable in it (the field names may have moved β€” see "
2804
+ "instagram-capture.md)")
2805
+ cnode, cnote = bd_corpus_profile(handle, log=log)
2806
+ if cnode:
2807
+ node = cnode
2808
+ profile = _bd_profile(node, handle)
2809
+ via = "brightdata:corpus"
2810
+ # ⚠ The corpus is PRE-COLLECTED, so the row is the vendor's last capture rather than
2811
+ # a read taken now. Said out loud on the attempt, because "when was this true" is the
2812
+ # one question a stored measurement must always be able to answer.
2813
+ note = (f"the live scrape did not answer ({_s(scrape_note, 120)}), so this came from "
2814
+ f"the vendor's pre-collected profile corpus β€” the values are its last "
2815
+ f"capture, not a read taken just now")
2816
+ else:
2817
+ return {"state": "blocked", "profile": {}, "posts": [], "via": "brightdata",
2818
+ "note": f"{scrape_note}; the profile corpus did not answer either ({cnote})"}
2819
  if profile.get("followers") is None and profile.get("following") is None:
2820
  # It answered 200 with something we could not read. Say THAT β€” not "0 followers".
2821
  # ⚠ `posts_count` is deliberately NOT part of this test: it arrives as a fabricated 0 and
 
2854
  # IDENTITY WITHOUT MEDIA IS STILL `partial`, on the paid rung too. The rule does not soften
2855
  # because we are paying: a run that wrote a follower count and no posts must not paint green
2856
  # over a posts table that did not grow.
2857
+ # ⚠ `via` CARRIES WHICH ROUTE ANSWERED β€” `brightdata` or `brightdata:corpus`. It lands in the
2858
+ # snapshot row's `source` column, so a stored measurement can always say whether it was read
2859
+ # live or taken from the vendor's pre-collected corpus. Two routes writing one indisting-
2860
+ # uishable `source` would make the series unauditable exactly where it matters most.
2861
  return {"state": "ok" if posts else "partial", "profile": profile, "posts": posts,
2862
+ "via": via,
2863
  "note": note or ("" if posts else "profile read; no posts were returned")}
2864
 
2865