1- """Live agent-run endpoint — audit a real URL with A and B.
1+ """Async agent-run endpoints — audit a real URL with A and B via the job queue .
22
3- Runs both agents live (real page fetch + live SERP + Anthropic) on any URL +
4- keyword the user enters, and records the results so a later replay is free. There
5- is no answer key for an arbitrary real page, so this returns recommendations and
6- cost, not scores — the scored A-vs-B proof lives on the planted benchmark .
3+ `POST /run` enqueues a HIGH-priority job and returns a `job_id` immediately; the
4+ single worker runs both agents (B learning on), and the UI polls `GET /run/{job_id}`
5+ until it is done. Going through the queue means a user run jumps ahead of background
6+ learning, waiting at most for the one page already in flight .
77"""
88
99from __future__ import annotations
1010
11- import httpx
11+ import uuid
12+
1213from fastapi import APIRouter , HTTPException
1314from pydantic import BaseModel
1415
15- from app .agents .agent_a import run_agent_a
16- from app .agents .agent_b import run_agent_b
17- from app .core .exceptions import ReplayMissError
18- from app .memory .janitor import run_janitor
19- from app .models import AgentRunResult
20- from app .repositories .diary import DiaryRepository
21- from app .tools .fetch_page import FetchPageInput , FetchPageTool
22- from app .tools .registry import build_default_registry
16+ from app .jobs .queue import HIGH , Job , get_queue
17+ from app .models import RunJobRecord
18+ from app .repositories .run_results import RunResultRepository
2319
2420router = APIRouter (prefix = "/run" , tags = ["run" ])
2521
@@ -29,49 +25,29 @@ class RunRequest(BaseModel):
2925 keyword : str
3026
3127
32- class RunResponse (BaseModel ):
33- a : AgentRunResult
34- b : AgentRunResult
35- note : str | None = (
36- None # set when the page body couldn't be fetched (SERP-only run)
37- )
28+ class RunAccepted (BaseModel ):
29+ job_id : str
30+
3831
32+ @router .post ("" , response_model = RunAccepted , status_code = 202 )
33+ async def run (req : RunRequest ) -> RunAccepted :
34+ """Enqueue an A-vs-B audit; the UI polls GET /run/{job_id} for the result."""
35+ job_id = uuid .uuid4 ().hex
36+ await RunResultRepository ().create (
37+ RunJobRecord (job_id = job_id , url = req .url , keyword = req .keyword )
38+ )
39+ get_queue ().enqueue (
40+ Job (
41+ name = "run" , args = {"job_id" : job_id , "url" : req .url , "keyword" : req .keyword }
42+ ),
43+ priority = HIGH ,
44+ )
45+ return RunAccepted (job_id = job_id )
3946
40- @router .post ("" , response_model = RunResponse )
41- async def run (req : RunRequest ) -> RunResponse :
42- try :
43- # Probe the page once. A bot-block no longer fails the run — fetch_page degrades
44- # to a SERP-only audit and tells the user via `note`.
45- probe = await FetchPageTool (replay = False ).run (FetchPageInput (url = req .url ))
46- a = await run_agent_a (
47- req .url ,
48- req .keyword ,
49- registry = build_default_registry (replay = False ),
50- replay = False ,
51- )
52- # B learns on every live run: read the playbook, audit, jot one diary lesson…
53- b = await run_agent_b (
54- req .url ,
55- req .keyword ,
56- registry = build_default_registry (replay = False ),
57- replay = False ,
58- memory_on = True ,
59- learn = True ,
60- run_index = await DiaryRepository ().next_run (),
61- )
62- # …then consolidate the diary into the playbook so the NEXT run sees it.
63- await run_janitor ()
64- except httpx .HTTPError as exc :
65- # A page bot-block degrades gracefully (see fetch_page); this only fires for
66- # other live HTTP failures, e.g. the SERP call.
67- raise HTTPException (
68- status_code = 502 ,
69- detail = f"Live request failed: { exc .__class__ .__name__ } . Try again or a different page." ,
70- ) from exc
71- except ReplayMissError as exc :
72- raise HTTPException (status_code = 409 , detail = str (exc )) from exc
73- except RuntimeError as exc :
74- # e.g. live SERP needs SERP_API_KEY
75- raise HTTPException (status_code = 502 , detail = str (exc )) from exc
7647
77- return RunResponse (a = a , b = b , note = None if probe .fetch_ok else probe .fetch_note )
48+ @router .get ("/{job_id}" , response_model = RunJobRecord )
49+ async def run_status (job_id : str ) -> RunJobRecord :
50+ record = await RunResultRepository ().get (job_id )
51+ if record is None :
52+ raise HTTPException (status_code = 404 , detail = "No such run job." )
53+ return record
0 commit comments