zenefil commited on
Commit
495efa0
·
verified ·
1 Parent(s): a0156d2

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +15 -0
  2. api.py +85 -0
  3. requirements.txt +5 -0
  4. skill_adapter.py +45 -0
Dockerfile ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+ WORKDIR /app
3
+
4
+ RUN apt-get update && apt-get install -y --no-install-recommends build-essential \
5
+ && rm -rf /var/lib/apt/lists/*
6
+
7
+ COPY requirements.txt .
8
+ RUN pip install --no-cache-dir -r requirements.txt
9
+
10
+ COPY api.py .
11
+ COPY skill_adapter.py .
12
+
13
+ ENV PORT=7860
14
+ EXPOSE 7860
15
+ CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "7860"]
api.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from fastapi.responses import HTMLResponse
3
+ from pydantic import BaseModel
4
+ from typing import Optional, Literal
5
+ import asyncio, os, uuid, requests
6
+ from datetime import datetime
7
+ from skill_adapter import build_full_flow_plan, build_open_url_plan
8
+
9
+ app = FastAPI()
10
+
11
+ agents, queues, jobs, results, waiters = {}, {}, {}, {}, {}
12
+ ADMIN_SECRET = os.environ.get("ADMIN_SECRET")
13
+
14
+ def now(): return datetime.utcnow().isoformat()+"Z"
15
+
16
+ def cond_for(agent_id):
17
+ if agent_id not in waiters: waiters[agent_id]=asyncio.Condition()
18
+ return waiters[agent_id]
19
+
20
+ def push_job(agent_id, payload):
21
+ job_id = str(uuid.uuid4())
22
+ job = {"id":job_id,"agent_id":agent_id,"payload":payload,"status":"queued","created_at":now(),"updated_at":now()}
23
+ jobs[job_id]=job
24
+ queues.setdefault(agent_id,[]).append(job)
25
+ async def notify():
26
+ async with cond_for(agent_id): cond_for(agent_id).notify_all()
27
+ asyncio.create_task(notify())
28
+ return job
29
+
30
+ class RegisterBody(BaseModel): display_name:str
31
+ class RegisterResp(BaseModel): agent_id:str; token:str
32
+ class EnqueuePlanBody(BaseModel): agent_id:str; plan:list; admin_secret:Optional[str]=None
33
+ class SubmitResultBody(BaseModel):
34
+ agent_id:str; token:str; job_id:str
35
+ status:Literal["completed","failed"]; message:Optional[str]=None; artifacts:Optional[dict]=None
36
+ class FullFlowReq(BaseModel): agent_id:str; email:str; password:str; project_id:str; admin_secret:Optional[str]=None
37
+ class OpenURLReq(BaseModel): agent_id:str; url:str; admin_secret:Optional[str]=None
38
+
39
+ @app.post("/register", response_model=RegisterResp)
40
+ async def register(body:RegisterBody):
41
+ agent_id, token = str(uuid.uuid4()), str(uuid.uuid4())
42
+ agents[agent_id]={"agent_id":agent_id,"token":token,"display_name":body.display_name,"registered_at":now(),"last_seen":now()}
43
+ queues[agent_id]=[]
44
+ return RegisterResp(agent_id=agent_id, token=token)
45
+
46
+ @app.get("/agents")
47
+ async def list_agents(): return {"agents":list(agents.values())}
48
+
49
+ @app.get("/next-job")
50
+ async def next_job(agent_id:str, token:str, wait:int=25):
51
+ a=agents.get(agent_id)
52
+ if not a or a["token"]!=token: raise HTTPException(status_code=401, detail="bad agent/token")
53
+ a["last_seen"]=now(); q=queues.get(agent_id,[])
54
+ if q: job=q.pop(0); job["status"]="taken"; job["updated_at"]=now(); return {"job":job}
55
+ c=cond_for(agent_id)
56
+ try:
57
+ async with asyncio.timeout(wait):
58
+ async with c: await c.wait()
59
+ q=queues.get(agent_id,[])
60
+ if q: job=q.pop(0); job["status"]="taken"; job["updated_at"]=now(); return {"job":job}
61
+ except: pass
62
+ return {}
63
+
64
+ @app.post("/submit-result")
65
+ async def submit_result(body:SubmitResultBody):
66
+ j=jobs.get(body.job_id);
67
+ if not j: raise HTTPException(status_code=404, detail="no job")
68
+ j["status"]=body.status; j["updated_at"]=now()
69
+ results[body.job_id]={"status":body.status,"message":body.message,"artifacts":body.artifacts,"submitted_at":now()}
70
+ return {"ok":True}
71
+
72
+ @app.post("/skill/full-flow")
73
+ async def skill_full_flow(body:FullFlowReq):
74
+ if ADMIN_SECRET and body.admin_secret!=ADMIN_SECRET: raise HTTPException(status_code=401, detail="bad secret")
75
+ plan=build_full_flow_plan(body.email, body.password, body.project_id)
76
+ job=push_job(body.agent_id, {"type":"plan","steps":plan}); return {"job":job}
77
+
78
+ @app.post("/skill/open-url")
79
+ async def skill_open_url(body:OpenURLReq):
80
+ if ADMIN_SECRET and body.admin_secret!=ADMIN_SECRET: raise HTTPException(status_code=401, detail="bad secret")
81
+ plan=build_open_url_plan(body.url)
82
+ job=push_job(body.agent_id, {"type":"plan","steps":plan}); return {"job":job}
83
+
84
+ @app.get("/")
85
+ async def home(): return HTMLResponse("<h3>SkillFlow Server Running</h3>")
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi==0.111.0
2
+ uvicorn==0.30.1
3
+ pydantic==2.7.4
4
+ jinja2==3.1.4
5
+ requests==2.32.3
skill_adapter.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Dict
2
+
3
+ LAB_URL = "https://www.cloudskillsboost.google/course_templates/976/labs/550878"
4
+ SIGN_IN_URL = "https://accounts.google.com/signin/v2/identifier"
5
+ MARKETPLACE_URL = "https://console.cloud.google.com/marketplace/product/google/generativelanguage.googleapis.com"
6
+
7
+ def step(act, **kw) -> Dict:
8
+ d = {"act": act}; d.update(kw); return d
9
+
10
+ def wait_css(sel: str, timeout: int = 25) -> Dict:
11
+ return {"act": "wait", "by": "css", "sel": sel, "timeout": timeout}
12
+
13
+ def click_css(sel: str) -> Dict:
14
+ return {"act": "click", "by": "css", "sel": sel}
15
+
16
+ def type_css(sel: str, text: str) -> Dict:
17
+ return {"act": "type", "by": "css", "sel": sel, "text": text}
18
+
19
+ def screenshot(label: str) -> Dict:
20
+ return {"act": "screenshot", "label": label}
21
+
22
+ def click_xpath(xpath: str) -> Dict:
23
+ return {"act": "click", "by": "xpath", "sel": xpath}
24
+
25
+ def wait_xpath(xpath: str, timeout: int = 25) -> Dict:
26
+ return {"act": "wait", "by": "xpath", "sel": xpath, "timeout": timeout}
27
+
28
+ def build_open_url_plan(url: str) -> List[Dict]:
29
+ return [ step("get", sel=url), wait_css("body", 20), screenshot("opened") ]
30
+
31
+ def build_full_flow_plan(email: str, password: str, project_id: str) -> List[Dict]:
32
+ steps: List[Dict] = []
33
+ # NOTE: shortened for clarity; fill in with full plan steps from previous response
34
+ steps += [
35
+ step("get", sel=SIGN_IN_URL),
36
+ wait_css("input[type=email]"),
37
+ type_css("input[type=email]", email),
38
+ click_css("#identifierNext"),
39
+ wait_css("input[type=password]"),
40
+ type_css("input[type=password]", password),
41
+ click_css("#passwordNext"),
42
+ wait_css("body", 40),
43
+ screenshot("after_login")
44
+ ]
45
+ return steps