PocketSkye commited on
Commit
0242ab2
·
1 Parent(s): 1a6eb82

Initial deployment

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +33 -0
  2. Dockerfile +51 -0
  3. agents/__init__.py +0 -0
  4. agents/result_formatter/__init__.py +0 -0
  5. agents/result_formatter/agent.py +98 -0
  6. agents/workflow_generator/__init__.py +0 -0
  7. agents/workflow_generator/agent.py +198 -0
  8. agents/workflow_generator/parser.py +28 -0
  9. agents/workflow_validator/__init__.py +0 -0
  10. agents/workflow_validator/agent.py +105 -0
  11. api/__init__.py +0 -0
  12. api/auth_routes.py +107 -0
  13. api/execution_routes.py +159 -0
  14. api/schedule_routes.py +0 -0
  15. api/workflow_routes.py +145 -0
  16. browser/__init__.py +0 -0
  17. browser/playwright_client.py +30 -0
  18. browser/test_playwright.py +28 -0
  19. core/__init__.py +0 -0
  20. core/config.py +22 -0
  21. core/database.py +27 -0
  22. core/deps.py +50 -0
  23. core/security.py +42 -0
  24. executor/__init__.py +0 -0
  25. executor/action_registry.py +1 -0
  26. executor/actions/__init__.py +0 -0
  27. executor/actions/click_action.py +24 -0
  28. executor/actions/extract_action.py +27 -0
  29. executor/actions/fill_action.py +26 -0
  30. executor/actions/goto_action.py +21 -0
  31. executor/actions/save_results_action.py +21 -0
  32. executor/actions/search_action.py +31 -0
  33. executor/actions/wait_action.py +21 -0
  34. executor/executor.py +127 -0
  35. main.py +68 -0
  36. middleware/__init__.py +0 -0
  37. middleware/request_timer.py +33 -0
  38. models/__init__.py +0 -0
  39. models/execution_log.py +37 -0
  40. models/user.py +25 -0
  41. models/workflow.py +38 -0
  42. models/workflow_run.py +45 -0
  43. models/workflow_schedule.py +49 -0
  44. requirements.txt +25 -0
  45. scheduler/__init__.py +0 -0
  46. scheduler/poller.py +110 -0
  47. scheduler/queue.py +3 -0
  48. scheduler/scheduler.py +29 -0
  49. scheduler/worker.py +60 -0
  50. schemas/__init__.py +0 -0
.gitignore ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @"
2
+ # Virtual Environment
3
+ venv/
4
+ .venv/
5
+ env/
6
+
7
+ # Environment Variables
8
+ .env
9
+
10
+ # Python Cache
11
+ __pycache__/
12
+ *.pyc
13
+
14
+ # IDE
15
+ .vscode/
16
+ .idea/
17
+
18
+ # Logs
19
+ *.log
20
+
21
+ # OS Files
22
+ .DS_Store
23
+ Thumbs.db
24
+
25
+ # Build Files
26
+ dist/
27
+ build/
28
+ *.egg-info/
29
+
30
+ # Playwright Reports
31
+ playwright-report/
32
+ test-results/
33
+ "@ | Out-File .gitignore
Dockerfile ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ # Prevent Python from buffering stdout/stderr
4
+ ENV PYTHONUNBUFFERED=1
5
+
6
+ # Install system dependencies required by Playwright
7
+ RUN apt-get update && apt-get install -y \
8
+ curl \
9
+ wget \
10
+ git \
11
+ libnss3 \
12
+ libatk1.0-0 \
13
+ libatk-bridge2.0-0 \
14
+ libcups2 \
15
+ libdrm2 \
16
+ libxkbcommon0 \
17
+ libxcomposite1 \
18
+ libxdamage1 \
19
+ libxfixes3 \
20
+ libxrandr2 \
21
+ libgbm1 \
22
+ libasound2 \
23
+ libpangocairo-1.0-0 \
24
+ libpango-1.0-0 \
25
+ libcairo2 \
26
+ libx11-6 \
27
+ libxcb1 \
28
+ libxext6 \
29
+ libx11-xcb1 \
30
+ libxcursor1 \
31
+ libxi6 \
32
+ libxtst6 \
33
+ fonts-liberation \
34
+ ca-certificates \
35
+ && rm -rf /var/lib/apt/lists/*
36
+
37
+ WORKDIR /app
38
+
39
+ # Install Python dependencies
40
+ COPY requirements.txt .
41
+ RUN pip install --no-cache-dir -r requirements.txt
42
+
43
+ # Install Playwright browser
44
+ RUN playwright install chromium
45
+
46
+ # Copy project
47
+ COPY . .
48
+
49
+ EXPOSE 7860
50
+
51
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
agents/__init__.py ADDED
File without changes
agents/result_formatter/__init__.py ADDED
File without changes
agents/result_formatter/agent.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_openai import ChatOpenAI
2
+ from langchain_core.prompts import (
3
+ ChatPromptTemplate
4
+ )
5
+ from langchain_core.output_parsers import (
6
+ StrOutputParser
7
+ )
8
+
9
+ from core.config import settings
10
+
11
+
12
+ class ResultFormatterAgent:
13
+
14
+ def __init__(self):
15
+
16
+ self.llm = ChatOpenAI(
17
+ api_key=settings.MISTRAL_API_KEY,
18
+ base_url="https://api.mistral.ai/v1",
19
+ model="mistral-medium-latest",
20
+ temperature=0.1
21
+ )
22
+
23
+ self.prompt = ChatPromptTemplate.from_messages(
24
+ [
25
+ (
26
+ "system",
27
+ """
28
+ You are an expert data analyst.
29
+
30
+ Your job is to analyze browser automation
31
+ results and present them in a concise,
32
+ human-friendly format.
33
+
34
+ Rules:
35
+
36
+ 1. Focus only on information relevant
37
+ to the user's request.
38
+
39
+ 2. Ignore:
40
+ - navigation menus
41
+ - headers
42
+ - footers
43
+ - ads
44
+ - irrelevant page text
45
+
46
+ 3. Summarize findings clearly.
47
+
48
+ 4. If products are found:
49
+ - list the most relevant ones
50
+ - mention prices if available
51
+
52
+ 5. If jobs are found:
53
+ - list job titles
54
+ - company names
55
+ - locations
56
+
57
+ 6. If no useful information exists,
58
+ say so clearly.
59
+
60
+ Return readable markdown.
61
+ """
62
+ ),
63
+ (
64
+ "user",
65
+ """
66
+ User Request:
67
+ {user_prompt}
68
+
69
+ Workflow Name:
70
+ {workflow_name}
71
+
72
+ Raw Results:
73
+ {results}
74
+ """
75
+ )
76
+ ]
77
+ )
78
+
79
+ self.chain = (
80
+ self.prompt
81
+ | self.llm
82
+ | StrOutputParser()
83
+ )
84
+
85
+ def format(
86
+ self,
87
+ user_prompt: str,
88
+ workflow_name: str,
89
+ results: str
90
+ ):
91
+
92
+ return self.chain.invoke(
93
+ {
94
+ "user_prompt": user_prompt,
95
+ "workflow_name": workflow_name,
96
+ "results": results
97
+ }
98
+ )
agents/workflow_generator/__init__.py ADDED
File without changes
agents/workflow_generator/agent.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_openai import ChatOpenAI
2
+ from langchain_core.prompts import ChatPromptTemplate
3
+ from langchain_core.output_parsers import StrOutputParser
4
+
5
+ from core.config import settings
6
+
7
+
8
+ class WorkflowGeneratorAgent:
9
+
10
+ def __init__(self):
11
+
12
+ self.llm = ChatOpenAI(
13
+ api_key=settings.MISTRAL_API_KEY,
14
+ base_url="https://api.mistral.ai/v1",
15
+ model="mistral-medium-latest",
16
+ temperature=0.2
17
+ )
18
+
19
+ self.prompt = ChatPromptTemplate.from_messages(
20
+ [
21
+ (
22
+ "system",
23
+ """
24
+ You are an expert browser automation workflow planner.
25
+
26
+ Convert user requests into valid JSON workflows.
27
+
28
+ Allowed actions:
29
+
30
+ - goto
31
+ - click
32
+ - fill
33
+ - wait
34
+ - search
35
+ - extract
36
+
37
+ Rules:
38
+
39
+ 1. execution_type must be:
40
+ - instant
41
+ - scheduled
42
+
43
+ 2. If the user wants information, products,
44
+ jobs, prices, news, search results,
45
+ or any data from a website:
46
+
47
+ Generate:
48
+
49
+ goto
50
+ search
51
+ wait
52
+ extract
53
+
54
+ 3. Default wait duration is 3 seconds.
55
+
56
+ 4. If the user does not specify the number
57
+ of results to return, use extract limit = 5.
58
+
59
+ 5. If the user specifies a number,
60
+ use that number as the extract limit.
61
+
62
+ 6. If the user mentions a website,
63
+ generate the correct URL.
64
+
65
+ 7. If the request contains scheduling,
66
+ set execution_type to scheduled.
67
+
68
+ Schedule format:
69
+
70
+ {{
71
+ "type": "daily" | "weekly" | "once",
72
+ "time": "HH:MM"
73
+ }}
74
+
75
+ For weekly schedules you MUST include:
76
+
77
+ {{
78
+ "type": "weekly",
79
+ "day_of_week": "monday",
80
+ "time": "09:00"
81
+ }}
82
+
83
+ Valid values for day_of_week:
84
+
85
+ - monday
86
+ - tuesday
87
+ - wednesday
88
+ - thursday
89
+ - friday
90
+ - saturday
91
+ - sunday
92
+
93
+ Example:
94
+
95
+ Request:
96
+ Every Monday at 9 AM search Amazon for AMD laptops
97
+
98
+ Output:
99
+
100
+ {{
101
+ "workflow_name": "weekly_amd_laptops_search",
102
+ "execution_type": "scheduled",
103
+ "schedule": {{
104
+ "type": "weekly",
105
+ "day_of_week": "monday",
106
+ "time": "09:00"
107
+ }},
108
+ "steps": [
109
+ {{
110
+ "action": "goto",
111
+ "url": "https://www.amazon.com"
112
+ }},
113
+ {{
114
+ "action": "search",
115
+ "query": "AMD laptops"
116
+ }},
117
+ {{
118
+ "action": "wait",
119
+ "duration": 3
120
+ }},
121
+ {{
122
+ "action": "extract",
123
+ "limit": 5
124
+ }}
125
+ ]
126
+ }}
127
+
128
+ Request:
129
+ Every day at 8 AM check iPhone prices
130
+
131
+ Output:
132
+
133
+ {{
134
+ "workflow_name": "daily_iphone_price_check",
135
+ "execution_type": "scheduled",
136
+ "schedule": {{
137
+ "type": "daily",
138
+ "time": "08:00"
139
+ }},
140
+ "steps": [
141
+ {{
142
+ "action": "goto",
143
+ "url": "https://www.amazon.com"
144
+ }},
145
+ {{
146
+ "action": "search",
147
+ "query": "iPhone"
148
+ }},
149
+ {{
150
+ "action": "wait",
151
+ "duration": 3
152
+ }},
153
+ {{
154
+ "action": "extract",
155
+ "limit": 5
156
+ }}
157
+ ]
158
+ }}
159
+
160
+ 8. Create meaningful workflow names.
161
+
162
+ 9. Return ONLY valid JSON.
163
+
164
+ 10. Use "url" for goto actions.
165
+ Never use "target" for goto actions.
166
+
167
+ Output format:
168
+
169
+ {{
170
+ "workflow_name": "",
171
+ "execution_type": "instant",
172
+ "schedule": null,
173
+ "steps": []
174
+ }}
175
+
176
+ Return JSON only.
177
+ """
178
+ ),
179
+ ("user", "{prompt}")
180
+ ]
181
+ )
182
+
183
+ self.chain = (
184
+ self.prompt
185
+ | self.llm
186
+ | StrOutputParser()
187
+ )
188
+
189
+ def generate(
190
+ self,
191
+ prompt: str
192
+ ):
193
+
194
+ return self.chain.invoke(
195
+ {
196
+ "prompt": prompt
197
+ }
198
+ )
agents/workflow_generator/parser.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+
3
+
4
+ def parse_workflow(response: str):
5
+
6
+ try:
7
+
8
+ response = response.strip()
9
+
10
+ if response.startswith("```json"):
11
+ response = response.replace(
12
+ "```json",
13
+ ""
14
+ )
15
+
16
+ if response.endswith("```"):
17
+ response = response[:-3]
18
+
19
+ response = response.strip()
20
+
21
+ return json.loads(response)
22
+
23
+ except Exception as e:
24
+
25
+ return {
26
+ "error": str(e),
27
+ "raw_response": response
28
+ }
agents/workflow_validator/__init__.py ADDED
File without changes
agents/workflow_validator/agent.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_openai import ChatOpenAI
2
+ from langchain_core.prompts import ChatPromptTemplate
3
+ from langchain_core.output_parsers import JsonOutputParser
4
+
5
+ from core.config import settings
6
+
7
+
8
+ class WorkflowValidatorAgent:
9
+
10
+ def __init__(self):
11
+
12
+ self.llm = ChatOpenAI(
13
+ api_key=settings.MISTRAL_API_KEY,
14
+ base_url="https://api.mistral.ai/v1",
15
+ model="mistral-medium-latest",
16
+ temperature=0
17
+ )
18
+
19
+ self.prompt = ChatPromptTemplate.from_messages(
20
+ [
21
+ (
22
+ "system",
23
+ """
24
+ You are a workflow validation agent.
25
+
26
+ Validate the workflow JSON.
27
+
28
+ Checks:
29
+
30
+ 1. workflow_name exists.
31
+
32
+ 2. execution_type exists and is:
33
+ - instant
34
+ - scheduled
35
+
36
+ 3. steps exists and contains at least
37
+ one action.
38
+
39
+ 4. Allowed actions:
40
+
41
+ - goto
42
+ - click
43
+ - fill
44
+ - wait
45
+ - search
46
+ - extract
47
+
48
+ 5. If execution_type is scheduled,
49
+ schedule must exist.
50
+
51
+ 6. If action is goto,
52
+ url must exist.
53
+
54
+ 7. If action is search,
55
+ query must exist.
56
+
57
+ 8. If action is fill,
58
+ selector and value must exist.
59
+
60
+ 9. If action is click,
61
+ selector must exist.
62
+
63
+ 10. If action is wait,
64
+ duration must exist.
65
+
66
+ 11. If action is extract and limit
67
+ is missing, automatically set:
68
+
69
+ {{
70
+ "limit": 5
71
+ }}
72
+
73
+ 12. Fix minor issues if possible.
74
+
75
+ Return ONLY JSON.
76
+
77
+ Format:
78
+
79
+ {{
80
+ "valid": true,
81
+ "message": "",
82
+ "workflow": {{}}
83
+ }}
84
+ """
85
+ ),
86
+ ("user", "{workflow}")
87
+ ]
88
+ )
89
+
90
+ self.chain = (
91
+ self.prompt
92
+ | self.llm
93
+ | JsonOutputParser()
94
+ )
95
+
96
+ def validate(
97
+ self,
98
+ workflow: dict
99
+ ):
100
+
101
+ return self.chain.invoke(
102
+ {
103
+ "workflow": workflow
104
+ }
105
+ )
api/__init__.py ADDED
File without changes
api/auth_routes.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+ from fastapi import Depends
3
+ from fastapi import HTTPException
4
+
5
+ from sqlalchemy.orm import Session
6
+
7
+ from core.database import get_db
8
+ from core.security import create_access_token
9
+ from core.deps import get_current_user
10
+ from models.user import User
11
+ from services.auth_service import (
12
+ get_user_by_email,
13
+ create_user,
14
+ authenticate_user
15
+ )
16
+
17
+ from schemas.auth_schema import (
18
+ RegisterRequest,
19
+ LoginRequest,
20
+ TokenResponse
21
+ )
22
+
23
+ # All authentication related APIs will have /auth prefix
24
+ router = APIRouter(
25
+ prefix="/auth",
26
+ tags=["Authentication"]
27
+ )
28
+
29
+
30
+ # Register a new user
31
+ @router.post("/register")
32
+ def register(
33
+ payload: RegisterRequest,
34
+ db: Session = Depends(get_db)
35
+ ):
36
+ # Check if email is already registered
37
+ existing_user = get_user_by_email(
38
+ db,
39
+ payload.email
40
+ )
41
+
42
+ if existing_user:
43
+ raise HTTPException(
44
+ status_code=400,
45
+ detail="Email already registered"
46
+ )
47
+
48
+ # Create user after hashing the password
49
+ user = create_user(
50
+ db,
51
+ payload.name,
52
+ payload.email,
53
+ payload.password
54
+ )
55
+
56
+ return {
57
+ "message": "User registered",
58
+ "user_id": user.id
59
+ }
60
+
61
+
62
+ # Login user and generate JWT access token
63
+ @router.post(
64
+ "/login",
65
+ response_model=TokenResponse
66
+ )
67
+ def login(
68
+ payload: LoginRequest,
69
+ db: Session = Depends(get_db)
70
+ ):
71
+ # Verify email and password
72
+ user = authenticate_user(
73
+ db,
74
+ payload.email,
75
+ payload.password
76
+ )
77
+
78
+ if not user:
79
+ raise HTTPException(
80
+ status_code=401,
81
+ detail="Invalid credentials"
82
+ )
83
+
84
+ # Store user information inside JWT payload
85
+ token = create_access_token(
86
+ {
87
+ "sub": str(user.id),
88
+ "email": user.email
89
+ }
90
+ )
91
+
92
+ return {
93
+ "access_token": token,
94
+ "token_type": "bearer"
95
+ }
96
+
97
+
98
+ # Returns details of the currently logged-in user
99
+ @router.get("/me")
100
+ def get_me(
101
+ current_user: User = Depends(get_current_user)
102
+ ):
103
+ return {
104
+ "id": current_user.id,
105
+ "name": current_user.name,
106
+ "email": current_user.email
107
+ }
api/execution_routes.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import (
2
+ APIRouter,
3
+ Depends,
4
+ HTTPException
5
+ )
6
+
7
+ from sqlalchemy.orm import Session
8
+
9
+ from core.database import get_db
10
+ from core.deps import get_current_user
11
+
12
+ from services.execution_service import (
13
+ ExecutionService
14
+ )
15
+
16
+ from models.workflow import Workflow
17
+ from models.workflow_run import WorkflowRun
18
+
19
+
20
+ router = APIRouter(
21
+ prefix="/executions",
22
+ tags=["Executions"]
23
+ )
24
+
25
+ execution_service = ExecutionService()
26
+
27
+
28
+ # RUN WORKFLOW
29
+
30
+ @router.post("/run/{workflow_id}")
31
+ async def run_workflow(
32
+ workflow_id: int,
33
+ db: Session = Depends(get_db),
34
+ current_user = Depends(
35
+ get_current_user
36
+ )
37
+ ):
38
+
39
+ return await execution_service.run_workflow(
40
+ workflow_id,
41
+ current_user.id,
42
+ db
43
+ )
44
+
45
+
46
+ # GET ALL EXECUTIONS OF CURRENT USER
47
+
48
+ @router.get("/history")
49
+ def get_execution_history(
50
+ db: Session = Depends(get_db),
51
+ current_user = Depends(
52
+ get_current_user
53
+ )
54
+ ):
55
+
56
+ runs = (
57
+ db.query(
58
+ WorkflowRun
59
+ )
60
+ .join(
61
+ Workflow,
62
+ Workflow.id ==
63
+ WorkflowRun.workflow_id
64
+ )
65
+ .filter(
66
+ Workflow.user_id ==
67
+ current_user.id
68
+ )
69
+ .order_by(
70
+ WorkflowRun.started_at.desc()
71
+ )
72
+ .all()
73
+ )
74
+
75
+ return runs
76
+
77
+
78
+ # GET EXECUTION HISTORY OF A WORKFLOW
79
+
80
+ @router.get(
81
+ "/workflow/{workflow_id}"
82
+ )
83
+ def get_workflow_runs(
84
+ workflow_id: int,
85
+ db: Session = Depends(get_db),
86
+ current_user = Depends(
87
+ get_current_user
88
+ )
89
+ ):
90
+
91
+ workflow = db.query(
92
+ Workflow
93
+ ).filter(
94
+ Workflow.id == workflow_id,
95
+ Workflow.user_id ==
96
+ current_user.id
97
+ ).first()
98
+
99
+ if not workflow:
100
+
101
+ raise HTTPException(
102
+ status_code=404,
103
+ detail="Workflow not found"
104
+ )
105
+
106
+ runs = (
107
+ db.query(
108
+ WorkflowRun
109
+ )
110
+ .filter(
111
+ WorkflowRun.workflow_id ==
112
+ workflow_id
113
+ )
114
+ .order_by(
115
+ WorkflowRun.started_at.desc()
116
+ )
117
+ .all()
118
+ )
119
+
120
+ return runs
121
+
122
+
123
+ # GET SINGLE EXECUTION
124
+
125
+ @router.get("/{run_id}")
126
+ def get_execution(
127
+ run_id: int,
128
+ db: Session = Depends(get_db),
129
+ current_user = Depends(
130
+ get_current_user
131
+ )
132
+ ):
133
+
134
+ run = (
135
+ db.query(
136
+ WorkflowRun
137
+ )
138
+ .join(
139
+ Workflow,
140
+ Workflow.id ==
141
+ WorkflowRun.workflow_id
142
+ )
143
+ .filter(
144
+ WorkflowRun.id ==
145
+ run_id,
146
+ Workflow.user_id ==
147
+ current_user.id
148
+ )
149
+ .first()
150
+ )
151
+
152
+ if not run:
153
+
154
+ raise HTTPException(
155
+ status_code=404,
156
+ detail="Execution not found"
157
+ )
158
+
159
+ return run
api/schedule_routes.py ADDED
File without changes
api/workflow_routes.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from sqlalchemy.orm import Session
3
+
4
+ from core.database import get_db
5
+ from core.deps import get_current_user
6
+
7
+ from models.workflow import Workflow
8
+ from models.user import User
9
+ from models.workflow_schedule import WorkflowSchedule
10
+ from models.workflow_run import WorkflowRun
11
+ from models.execution_log import ExecutionLog
12
+
13
+ from schemas.workflow_schema import WorkflowRequest
14
+
15
+ from services.workflow_service import WorkflowService
16
+
17
+
18
+ router = APIRouter(
19
+ prefix="/workflows",
20
+ tags=["Workflows"]
21
+ )
22
+
23
+ workflow_service = WorkflowService()
24
+
25
+
26
+ # CREATE WORKFLOW
27
+
28
+ @router.post("/")
29
+ def create_workflow(
30
+ request: WorkflowRequest,
31
+ db: Session = Depends(get_db),
32
+ current_user: User = Depends(get_current_user)
33
+ ):
34
+
35
+ return workflow_service.create_workflow(
36
+ prompt=request.prompt,
37
+ user_id=current_user.id,
38
+ db=db
39
+ )
40
+
41
+
42
+ # GET ALL WORKFLOWS (USER ONLY)
43
+
44
+ @router.get("")
45
+ def get_workflows(
46
+ db: Session = Depends(get_db),
47
+ current_user: User = Depends(get_current_user)
48
+ ):
49
+
50
+ workflows = db.query(
51
+ Workflow
52
+ ).filter(
53
+ Workflow.user_id == current_user.id
54
+ ).all()
55
+
56
+ return workflows
57
+
58
+
59
+ # GET SINGLE WORKFLOW
60
+
61
+ @router.get("/{workflow_id}")
62
+ def get_workflow(
63
+ workflow_id: int,
64
+ db: Session = Depends(get_db),
65
+ current_user: User = Depends(get_current_user)
66
+ ):
67
+
68
+ workflow = db.query(
69
+ Workflow
70
+ ).filter(
71
+ Workflow.id == workflow_id,
72
+ Workflow.user_id == current_user.id
73
+ ).first()
74
+
75
+ if not workflow:
76
+
77
+ raise HTTPException(
78
+ status_code=404,
79
+ detail="Workflow not found"
80
+ )
81
+
82
+ return workflow
83
+
84
+
85
+ # DELETE WORKFLOW
86
+
87
+ @router.delete("/{workflow_id}")
88
+ def delete_workflow(
89
+ workflow_id: int,
90
+ db: Session = Depends(get_db),
91
+ current_user: User = Depends(get_current_user)
92
+ ):
93
+
94
+ workflow = db.query(
95
+ Workflow
96
+ ).filter(
97
+ Workflow.id == workflow_id,
98
+ Workflow.user_id == current_user.id
99
+ ).first()
100
+
101
+ if not workflow:
102
+
103
+ raise HTTPException(
104
+ status_code=404,
105
+ detail="Workflow not found"
106
+ )
107
+
108
+ # Fetch all runs for this workflow
109
+ runs = db.query(
110
+ WorkflowRun
111
+ ).filter(
112
+ WorkflowRun.workflow_id == workflow.id
113
+ ).all()
114
+
115
+ # Delete execution logs first
116
+ for run in runs:
117
+
118
+ db.query(
119
+ ExecutionLog
120
+ ).filter(
121
+ ExecutionLog.run_id == run.id
122
+ ).delete()
123
+
124
+ # Delete workflow runs
125
+ db.query(
126
+ WorkflowRun
127
+ ).filter(
128
+ WorkflowRun.workflow_id == workflow.id
129
+ ).delete()
130
+
131
+ # Delete workflow schedules
132
+ db.query(
133
+ WorkflowSchedule
134
+ ).filter(
135
+ WorkflowSchedule.workflow_id == workflow.id
136
+ ).delete()
137
+
138
+ # Delete workflow
139
+ db.delete(workflow)
140
+
141
+ db.commit()
142
+
143
+ return {
144
+ "message": "Workflow deleted"
145
+ }
browser/__init__.py ADDED
File without changes
browser/playwright_client.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from playwright.async_api import (
2
+ async_playwright
3
+ )
4
+
5
+
6
+ class PlaywrightClient:
7
+
8
+ async def get_page(self):
9
+
10
+ # Start the Playwright engine
11
+ playwright = await (
12
+ async_playwright().start()
13
+ )
14
+
15
+ # Launch a headless Chromium browser
16
+ browser = await (
17
+ playwright.chromium.launch(
18
+ headless=True
19
+ )
20
+ )
21
+
22
+ # Open a new browser tab
23
+ page = await browser.new_page()
24
+
25
+ # Return all objects so they can be used and closed later
26
+ return (
27
+ playwright,
28
+ browser,
29
+ page
30
+ )
browser/test_playwright.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from playwright.async_api import async_playwright
3
+
4
+
5
+ async def main():
6
+
7
+ playwright = await async_playwright().start()
8
+
9
+ browser = await playwright.chromium.launch(
10
+ headless=False
11
+ )
12
+
13
+ page = await browser.new_page()
14
+
15
+ await page.goto(
16
+ "https://google.com"
17
+ )
18
+
19
+ await page.wait_for_timeout(
20
+ 5000
21
+ )
22
+
23
+ await browser.close()
24
+
25
+ await playwright.stop()
26
+
27
+
28
+ asyncio.run(main())
core/__init__.py ADDED
File without changes
core/config.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ from pydantic_settings import BaseSettings, SettingsConfigDict
3
+
4
+ BASE_DIR = Path(__file__).resolve().parent.parent
5
+
6
+ class Settings(BaseSettings):
7
+ DATABASE_URL: str
8
+ SECRET_KEY: str
9
+ ALGORITHM: str
10
+ ACCESS_TOKEN_EXPIRE_MINUTES: int
11
+ MISTRAL_API_KEY: str
12
+
13
+ APP_NAME: str = "TaskWeaver"
14
+
15
+ model_config = SettingsConfigDict(
16
+ env_file=BASE_DIR / ".env",
17
+ extra="ignore"
18
+ )
19
+
20
+ settings = Settings()
21
+
22
+ print("Loaded successfully")
core/database.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import create_engine
2
+ from sqlalchemy.orm import declarative_base
3
+ from sqlalchemy.orm import sessionmaker
4
+
5
+ from core.config import settings
6
+
7
+ engine = create_engine(
8
+ settings.DATABASE_URL,
9
+ pool_pre_ping=True
10
+ )
11
+
12
+ SessionLocal = sessionmaker(
13
+ autocommit=False,
14
+ autoflush=False,
15
+ bind=engine
16
+ )
17
+
18
+ Base = declarative_base()
19
+
20
+
21
+ def get_db():
22
+ db = SessionLocal()
23
+
24
+ try:
25
+ yield db
26
+ finally:
27
+ db.close()
core/deps.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import Depends, HTTPException, status
2
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
3
+
4
+ from jose import jwt, JWTError
5
+
6
+ from sqlalchemy.orm import Session
7
+
8
+ from core.config import settings
9
+ from core.database import get_db
10
+ from models.user import User
11
+
12
+ security = HTTPBearer()
13
+
14
+
15
+ def get_current_user(
16
+ credentials: HTTPAuthorizationCredentials = Depends(security),
17
+ db: Session = Depends(get_db)
18
+ ):
19
+ token = credentials.credentials
20
+
21
+ try:
22
+ payload = jwt.decode(
23
+ token,
24
+ settings.SECRET_KEY,
25
+ algorithms=[settings.ALGORITHM]
26
+ )
27
+
28
+ user_id: str = payload.get("sub")
29
+
30
+ if user_id is None:
31
+ raise HTTPException(
32
+ status_code=401,
33
+ detail="Invalid token"
34
+ )
35
+
36
+ except JWTError:
37
+ raise HTTPException(
38
+ status_code=401,
39
+ detail="Token is invalid or expired"
40
+ )
41
+
42
+ user = db.query(User).filter(User.id == int(user_id)).first()
43
+
44
+ if not user:
45
+ raise HTTPException(
46
+ status_code=404,
47
+ detail="User not found"
48
+ )
49
+
50
+ return user
core/security.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime, timedelta
2
+
3
+ from jose import jwt
4
+ from passlib.context import CryptContext
5
+
6
+ from core.config import settings
7
+
8
+
9
+ pwd_context = CryptContext(
10
+ schemes=["bcrypt"],
11
+ deprecated="auto"
12
+ )
13
+
14
+
15
+ def hash_password(password: str) -> str:
16
+ return pwd_context.hash(password)
17
+
18
+
19
+ def verify_password(
20
+ plain_password: str,
21
+ hashed_password: str
22
+ ) -> bool:
23
+ return pwd_context.verify(
24
+ plain_password,
25
+ hashed_password
26
+ )
27
+
28
+
29
+ def create_access_token(data: dict):
30
+ to_encode = data.copy()
31
+
32
+ expire = datetime.utcnow() + timedelta(
33
+ minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
34
+ )
35
+
36
+ to_encode.update({"exp": expire})
37
+
38
+ return jwt.encode(
39
+ to_encode,
40
+ settings.SECRET_KEY,
41
+ algorithm=settings.ALGORITHM
42
+ )
executor/__init__.py ADDED
File without changes
executor/action_registry.py ADDED
@@ -0,0 +1 @@
 
 
1
+ ACTION_REGISTRY = {}
executor/actions/__init__.py ADDED
File without changes
executor/actions/click_action.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from executor.action_registry import (
2
+ ACTION_REGISTRY
3
+ )
4
+ """{
5
+ "action": "click",
6
+ "selector": "#submit"
7
+ }"""
8
+
9
+ async def execute(
10
+ page,
11
+ step
12
+ ):
13
+
14
+ await page.click(
15
+ step["selector"]
16
+ )
17
+
18
+ return {
19
+ "action": "click",
20
+ "status": "success"
21
+ }
22
+
23
+
24
+ ACTION_REGISTRY["click"] = execute
executor/actions/extract_action.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from executor.action_registry import (
2
+ ACTION_REGISTRY
3
+ )
4
+
5
+
6
+ async def execute(
7
+ page,
8
+ step
9
+ ):
10
+
11
+ body_text = await page.locator(
12
+ "body"
13
+ ).inner_text()
14
+
15
+ max_chars = step.get(
16
+ "max_chars",
17
+ 12000
18
+ )
19
+
20
+ return {
21
+ "action": "extract",
22
+ "status": "success",
23
+ "data": body_text[:max_chars]
24
+ }
25
+
26
+
27
+ ACTION_REGISTRY["extract"] = execute
executor/actions/fill_action.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from executor.action_registry import (
2
+ ACTION_REGISTRY
3
+ )
4
+ """{
5
+ "action": "fill",
6
+ "selector": "#email",
7
+ "value": "abc@test.com"
8
+ }"""
9
+
10
+ async def execute(
11
+ page,
12
+ step
13
+ ):
14
+
15
+ await page.fill(
16
+ step["selector"],
17
+ step["value"]
18
+ )
19
+
20
+ return {
21
+ "action": "fill",
22
+ "status": "success"
23
+ }
24
+
25
+
26
+ ACTION_REGISTRY["fill"] = execute
executor/actions/goto_action.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from executor.action_registry import (
2
+ ACTION_REGISTRY
3
+ )
4
+
5
+
6
+ async def execute(
7
+ page,
8
+ step
9
+ ):
10
+
11
+ url = step.get("url") or step.get("target")
12
+
13
+ await page.goto(url)
14
+
15
+ return {
16
+ "action": "goto",
17
+ "status": "success"
18
+ }
19
+
20
+
21
+ ACTION_REGISTRY["goto"] = execute
executor/actions/save_results_action.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from executor.action_registry import (
2
+ ACTION_REGISTRY
3
+ )
4
+
5
+
6
+ async def execute(
7
+ page,
8
+ step
9
+ ):
10
+
11
+ print(
12
+ "SAVE RESULTS"
13
+ )
14
+
15
+ return {
16
+ "action": "save_results",
17
+ "status": "success"
18
+ }
19
+
20
+
21
+ ACTION_REGISTRY["save_results"] = execute
executor/actions/search_action.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from executor.action_registry import (
2
+ ACTION_REGISTRY
3
+ )
4
+
5
+
6
+ async def execute(
7
+ page,
8
+ step
9
+ ):
10
+
11
+ query = step["query"]
12
+
13
+ search_box = page.locator(
14
+ "input[type='search'], input[type='text']"
15
+ ).first
16
+
17
+ await search_box.fill(
18
+ query
19
+ )
20
+
21
+ await page.keyboard.press(
22
+ "Enter"
23
+ )
24
+
25
+ return {
26
+ "action": "search",
27
+ "status": "success"
28
+ }
29
+
30
+
31
+ ACTION_REGISTRY["search"] = execute
executor/actions/wait_action.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from executor.action_registry import (
2
+ ACTION_REGISTRY
3
+ )
4
+
5
+
6
+ async def execute(
7
+ page,
8
+ step
9
+ ):
10
+
11
+ await page.wait_for_timeout(
12
+ step["duration"] * 1000
13
+ )
14
+
15
+ return {
16
+ "action": "wait",
17
+ "status": "success"
18
+ }
19
+
20
+
21
+ ACTION_REGISTRY["wait"] = execute
executor/executor.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from executor.action_registry import (
2
+ ACTION_REGISTRY
3
+ )
4
+
5
+ # Importing action modules automatically registers them
6
+ # into ACTION_REGISTRY during module initialization.
7
+ import executor.actions.goto_action
8
+ import executor.actions.click_action
9
+ import executor.actions.fill_action
10
+ import executor.actions.wait_action
11
+ import executor.actions.extract_action
12
+ import executor.actions.save_results_action
13
+ import executor.actions.search_action
14
+
15
+ from models.execution_log import (
16
+ ExecutionLog
17
+ )
18
+
19
+
20
+ class WorkflowExecutor:
21
+
22
+ async def execute(
23
+ self,
24
+ page,
25
+ workflow,
26
+ db=None,
27
+ run_id=None
28
+ ):
29
+
30
+ # Store results returned by each workflow step
31
+ results = []
32
+
33
+ # Execute workflow steps one by one in the defined order
34
+ for index, step in enumerate(
35
+ workflow["steps"],
36
+ start=1
37
+ ):
38
+
39
+ action = step["action"]
40
+
41
+ print(
42
+ "ACTION_REGISTRY =",
43
+ ACTION_REGISTRY
44
+ )
45
+
46
+ print(
47
+ "ACTION =",
48
+ action
49
+ )
50
+
51
+ # Find the function corresponding to this action
52
+ handler = ACTION_REGISTRY.get(
53
+ action
54
+ )
55
+
56
+ if not handler:
57
+
58
+ # Log failure if action is unsupported
59
+ if db and run_id:
60
+
61
+ db.add(
62
+ ExecutionLog(
63
+ run_id=run_id,
64
+ step_number=index,
65
+ action=action,
66
+ status="failed",
67
+ message=f"Unknown action: {action}"
68
+ )
69
+ )
70
+
71
+ db.commit()
72
+
73
+ raise Exception(
74
+ f"Unknown action: {action}"
75
+ )
76
+
77
+ try:
78
+
79
+ # Execute the Playwright action
80
+ result = await handler(
81
+ page,
82
+ step
83
+ )
84
+
85
+ results.append(
86
+ result
87
+ )
88
+
89
+ # Store successful execution log
90
+ if db and run_id:
91
+
92
+ db.add(
93
+ ExecutionLog(
94
+ run_id=run_id,
95
+ step_number=index,
96
+ action=action,
97
+ status=result.get(
98
+ "status",
99
+ "success"
100
+ ),
101
+ message=f"{action} executed successfully"
102
+ )
103
+ )
104
+
105
+ db.commit()
106
+
107
+ except Exception as e:
108
+
109
+ # Log step failure before stopping workflow execution
110
+ if db and run_id:
111
+
112
+ db.add(
113
+ ExecutionLog(
114
+ run_id=run_id,
115
+ step_number=index,
116
+ action=action,
117
+ status="failed",
118
+ message=str(e)
119
+ )
120
+ )
121
+
122
+ db.commit()
123
+
124
+ raise e
125
+
126
+ # Return results collected from all workflow steps
127
+ return results
main.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+
3
+ # Required for Playwright on Windows
4
+ asyncio.set_event_loop_policy(
5
+ asyncio.WindowsProactorEventLoopPolicy()
6
+ )
7
+
8
+ from fastapi import FastAPI
9
+
10
+ from core.database import Base, engine
11
+
12
+ # Import models before creating tables
13
+ from models.user import User
14
+ from models.workflow import Workflow
15
+ from models.workflow_run import WorkflowRun
16
+ from models.execution_log import ExecutionLog
17
+ from models.workflow_schedule import WorkflowSchedule
18
+
19
+ # Import API routes
20
+ from api.workflow_routes import router as workflow_router
21
+ from api.auth_routes import router as auth_router
22
+ from api.execution_routes import (
23
+ router as execution_router
24
+ )
25
+
26
+ from fastapi.middleware.cors import (
27
+ CORSMiddleware
28
+ )
29
+
30
+ from middleware.request_timer import (
31
+ request_timer
32
+ )
33
+
34
+ from scheduler.scheduler import start_scheduler
35
+
36
+ # Create FastAPI application
37
+ app = FastAPI(
38
+ title="TaskWeaver",
39
+ version="1.0.0"
40
+ )
41
+
42
+ # Register request timing middleware
43
+ app.middleware("http")(
44
+ request_timer
45
+ )
46
+
47
+ # Create database tables
48
+ Base.metadata.create_all(bind=engine)
49
+
50
+ # Register all API routes
51
+ app.include_router(auth_router)
52
+ app.include_router(workflow_router)
53
+ app.include_router(execution_router)
54
+
55
+ # Start scheduler when the server starts
56
+ @app.on_event("startup")
57
+ def startup_event():
58
+ start_scheduler()
59
+
60
+ # Root endpoint
61
+ @app.get("/")
62
+ def root():
63
+ return {"message": "TaskWeaver API Running"}
64
+
65
+ # Health check endpoint
66
+ @app.get("/health")
67
+ def health():
68
+ return {"status": "healthy"}
middleware/__init__.py ADDED
File without changes
middleware/request_timer.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+
3
+ from fastapi import Request
4
+
5
+
6
+ async def request_timer(
7
+ request: Request,
8
+ call_next
9
+ ):
10
+
11
+ start_time = time.perf_counter()
12
+
13
+ response = await call_next(
14
+ request
15
+ )
16
+
17
+ process_time = (
18
+ time.perf_counter()
19
+ - start_time
20
+ )
21
+
22
+ response.headers[
23
+ "X-Process-Time"
24
+ ] = f"{process_time:.4f}"
25
+
26
+ print(
27
+ f"[{request.method}] "
28
+ f"{request.url.path} "
29
+ f"completed in "
30
+ f"{process_time:.4f}s"
31
+ )
32
+
33
+ return response
models/__init__.py ADDED
File without changes
models/execution_log.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+
3
+ from sqlalchemy import (
4
+ Column,
5
+ Integer,
6
+ String,
7
+ Text,
8
+ ForeignKey,
9
+ DateTime
10
+ )
11
+
12
+ from core.database import Base
13
+
14
+
15
+ class ExecutionLog(Base):
16
+ __tablename__ = "execution_logs"
17
+
18
+ id = Column(Integer, primary_key=True, index=True)
19
+
20
+ run_id = Column(
21
+ Integer,
22
+ ForeignKey("workflow_runs.id"),
23
+ nullable=False
24
+ )
25
+
26
+ step_number = Column(Integer)
27
+
28
+ action = Column(String(100))
29
+
30
+ status = Column(String(50))
31
+
32
+ message = Column(Text)
33
+
34
+ created_at = Column(
35
+ DateTime,
36
+ default=datetime.utcnow
37
+ )
models/user.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+
3
+ from sqlalchemy import Column, Integer, String, DateTime
4
+
5
+ from core.database import Base
6
+
7
+
8
+ class User(Base):
9
+ __tablename__ = "users"
10
+
11
+ id = Column(Integer, primary_key=True, index=True)
12
+
13
+ name = Column(String(100), nullable=False)
14
+
15
+ email = Column(String(255), unique=True, nullable=False)
16
+
17
+ password_hash = Column(String(255), nullable=False)
18
+
19
+ created_at = Column(DateTime, default=datetime.utcnow)
20
+
21
+ updated_at = Column(
22
+ DateTime,
23
+ default=datetime.utcnow,
24
+ onupdate=datetime.utcnow
25
+ )
models/workflow.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+
3
+ from sqlalchemy import (
4
+ Column,
5
+ Integer,
6
+ String,
7
+ Text,
8
+ ForeignKey,
9
+ DateTime
10
+ )
11
+
12
+ from core.database import Base
13
+
14
+
15
+ class Workflow(Base):
16
+ __tablename__ = "workflows"
17
+
18
+ id = Column(Integer, primary_key=True, index=True)
19
+
20
+ user_id = Column(
21
+ Integer,
22
+ ForeignKey("users.id"),
23
+ nullable=False
24
+ )
25
+
26
+ name = Column(String(255), nullable=False)
27
+
28
+ prompt = Column(Text, nullable=False)
29
+
30
+ workflow_json = Column(Text)
31
+
32
+ created_at = Column(DateTime, default=datetime.utcnow)
33
+
34
+ updated_at = Column(
35
+ DateTime,
36
+ default=datetime.utcnow,
37
+ onupdate=datetime.utcnow
38
+ )
models/workflow_run.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+
3
+ from sqlalchemy import (
4
+ Column,
5
+ Integer,
6
+ String,
7
+ Text,
8
+ ForeignKey,
9
+ DateTime,
10
+ Float
11
+ )
12
+
13
+ from core.database import Base
14
+
15
+
16
+ class WorkflowRun(Base):
17
+ __tablename__ = "workflow_runs"
18
+
19
+ id = Column(Integer, primary_key=True, index=True)
20
+
21
+ workflow_id = Column(
22
+ Integer,
23
+ ForeignKey("workflows.id"),
24
+ nullable=False
25
+ )
26
+
27
+ status = Column(
28
+ String(50),
29
+ default="pending"
30
+ )
31
+
32
+ started_at = Column(DateTime)
33
+
34
+ completed_at = Column(DateTime)
35
+
36
+ duration = Column(Float)
37
+
38
+ result_json = Column(Text)
39
+
40
+ error_message = Column(Text)
41
+
42
+ created_at = Column(
43
+ DateTime,
44
+ default=datetime.utcnow
45
+ )
models/workflow_schedule.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+
3
+ from sqlalchemy import (
4
+ Column,
5
+ Integer,
6
+ String,
7
+ Boolean,
8
+ ForeignKey,
9
+ DateTime
10
+ )
11
+
12
+ from core.database import Base
13
+
14
+
15
+ class WorkflowSchedule(Base):
16
+ __tablename__ = "workflow_schedules"
17
+
18
+ id = Column(Integer, primary_key=True, index=True)
19
+
20
+ workflow_id = Column(
21
+ Integer,
22
+ ForeignKey("workflows.id"),
23
+ nullable=False
24
+ )
25
+
26
+ schedule_type = Column(
27
+ String(50),
28
+ nullable=False
29
+ )
30
+
31
+ run_time = Column(
32
+ String(10)
33
+ )
34
+
35
+ day_of_week = Column(
36
+ String(20)
37
+ )
38
+
39
+ is_active = Column(
40
+ Boolean,
41
+ default=True
42
+ )
43
+
44
+ next_run_at = Column(DateTime)
45
+
46
+ created_at = Column(
47
+ DateTime,
48
+ default=datetime.utcnow
49
+ )
requirements.txt ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.116.1
2
+ uvicorn[standard]==0.35.0
3
+
4
+ sqlalchemy==2.0.43
5
+ psycopg2-binary==2.9.10
6
+
7
+ python-dotenv==1.1.1
8
+
9
+ python-jose[cryptography]==3.5.0
10
+ passlib[bcrypt]==1.7.4
11
+ bcrypt==4.1.2
12
+
13
+
14
+ pydantic==2.11.7
15
+ pydantic-settings==2.10.1
16
+
17
+ mistralai==1.9.10
18
+
19
+ playwright==1.54.0
20
+
21
+ apscheduler==3.11.0
22
+
23
+ loguru==0.7.3
24
+ email-validator
25
+
scheduler/__init__.py ADDED
File without changes
scheduler/poller.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime, timedelta
2
+
3
+ from core.database import SessionLocal
4
+
5
+ from models.workflow_schedule import WorkflowSchedule
6
+
7
+ from scheduler.queue import execution_queue
8
+
9
+
10
+ def calculate_next_run(schedule):
11
+
12
+ now = datetime.utcnow()
13
+
14
+ hour, minute = map(
15
+ int,
16
+ schedule.run_time.split(":")
17
+ )
18
+
19
+ if schedule.schedule_type == "daily":
20
+
21
+ next_run = now.replace(
22
+ hour=hour,
23
+ minute=minute,
24
+ second=0,
25
+ microsecond=0
26
+ )
27
+
28
+ if next_run <= now:
29
+ next_run += timedelta(days=1)
30
+
31
+ return next_run
32
+
33
+ if schedule.schedule_type == "weekly":
34
+
35
+ days = {
36
+ "monday": 0,
37
+ "tuesday": 1,
38
+ "wednesday": 2,
39
+ "thursday": 3,
40
+ "friday": 4,
41
+ "saturday": 5,
42
+ "sunday": 6
43
+ }
44
+
45
+ target_day = days[
46
+ schedule.day_of_week.lower()
47
+ ]
48
+
49
+ current_day = now.weekday()
50
+
51
+ days_ahead = (
52
+ target_day - current_day
53
+ ) % 7
54
+
55
+ next_run = now.replace(
56
+ hour=hour,
57
+ minute=minute,
58
+ second=0,
59
+ microsecond=0
60
+ ) + timedelta(
61
+ days=days_ahead
62
+ )
63
+
64
+ if next_run <= now:
65
+ next_run += timedelta(days=7)
66
+
67
+ return next_run
68
+
69
+ return None
70
+
71
+
72
+ def check_due_workflows():
73
+
74
+ db = SessionLocal()
75
+
76
+ try:
77
+
78
+ schedules = db.query(
79
+ WorkflowSchedule
80
+ ).filter(
81
+ WorkflowSchedule.is_active == True,
82
+ WorkflowSchedule.next_run_at <= datetime.utcnow()
83
+ ).all()
84
+
85
+ print(
86
+ f"Found {len(schedules)} due schedules"
87
+ )
88
+
89
+ for schedule in schedules:
90
+
91
+ execution_queue.put(
92
+ schedule.workflow_id
93
+ )
94
+
95
+ print(
96
+ f"Queued workflow {schedule.workflow_id}"
97
+ )
98
+
99
+ # move schedule forward immediately
100
+ schedule.next_run_at = (
101
+ calculate_next_run(
102
+ schedule
103
+ )
104
+ )
105
+
106
+ db.commit()
107
+
108
+ finally:
109
+
110
+ db.close()
scheduler/queue.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from queue import Queue
2
+
3
+ execution_queue = Queue()
scheduler/scheduler.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from apscheduler.schedulers.background import (
2
+ BackgroundScheduler
3
+ )
4
+
5
+ from scheduler.poller import (
6
+ check_due_workflows
7
+ )
8
+ from scheduler.worker import (
9
+ start_worker
10
+ )
11
+ scheduler = BackgroundScheduler()
12
+
13
+
14
+ def start_scheduler():
15
+
16
+ if not scheduler.running:
17
+ start_worker()
18
+
19
+ scheduler.add_job(
20
+ check_due_workflows,
21
+ "interval",
22
+ minutes=5
23
+ )
24
+
25
+ scheduler.start()
26
+
27
+ print(
28
+ "Scheduler started"
29
+ )
scheduler/worker.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import threading
2
+ import asyncio
3
+
4
+ from core.database import SessionLocal
5
+
6
+ from scheduler.queue import (
7
+ execution_queue
8
+ )
9
+
10
+ from services.execution_service import (
11
+ ExecutionService
12
+ )
13
+
14
+
15
+ def worker():
16
+
17
+ print("Worker started")
18
+
19
+ while True:
20
+
21
+ workflow_id = execution_queue.get()
22
+
23
+ print(
24
+ f"Executing workflow {workflow_id}"
25
+ )
26
+
27
+ db = SessionLocal()
28
+
29
+ try:
30
+
31
+ service = ExecutionService()
32
+
33
+ asyncio.run(
34
+ service.run_scheduled_workflow(
35
+ workflow_id,
36
+ db
37
+ )
38
+ )
39
+
40
+ except Exception as e:
41
+
42
+ print(
43
+ f"Worker error: {e}"
44
+ )
45
+
46
+ finally:
47
+
48
+ db.close()
49
+
50
+ execution_queue.task_done()
51
+
52
+
53
+ def start_worker():
54
+
55
+ thread = threading.Thread(
56
+ target=worker,
57
+ daemon=True
58
+ )
59
+
60
+ thread.start()
schemas/__init__.py ADDED
File without changes