optexity 0.1.2__py3-none-any.whl → 0.1.4__py3-none-any.whl

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.
Files changed (77) hide show
  1. optexity/cli.py +1 -1
  2. optexity/examples/__init__.py +0 -0
  3. optexity/examples/add_example.py +88 -0
  4. optexity/examples/download_pdf_url.py +29 -0
  5. optexity/examples/extract_price_stockanalysis.py +44 -0
  6. optexity/examples/file_upload.py +59 -0
  7. optexity/examples/i94.py +126 -0
  8. optexity/examples/i94_travel_history.py +126 -0
  9. optexity/examples/peachstate_medicaid.py +201 -0
  10. optexity/examples/supabase_login.py +75 -0
  11. optexity/inference/__init__.py +0 -0
  12. optexity/inference/agents/__init__.py +0 -0
  13. optexity/inference/agents/error_handler/__init__.py +0 -0
  14. optexity/inference/agents/error_handler/error_handler.py +39 -0
  15. optexity/inference/agents/error_handler/prompt.py +60 -0
  16. optexity/inference/agents/index_prediction/__init__.py +0 -0
  17. optexity/inference/agents/index_prediction/action_prediction_locator_axtree.py +45 -0
  18. optexity/inference/agents/index_prediction/prompt.py +14 -0
  19. optexity/inference/agents/select_value_prediction/__init__.py +0 -0
  20. optexity/inference/agents/select_value_prediction/prompt.py +20 -0
  21. optexity/inference/agents/select_value_prediction/select_value_prediction.py +39 -0
  22. optexity/inference/agents/two_fa_extraction/__init__.py +0 -0
  23. optexity/inference/agents/two_fa_extraction/prompt.py +23 -0
  24. optexity/inference/agents/two_fa_extraction/two_fa_extraction.py +47 -0
  25. optexity/inference/child_process.py +251 -0
  26. optexity/inference/core/__init__.py +0 -0
  27. optexity/inference/core/interaction/__init__.py +0 -0
  28. optexity/inference/core/interaction/handle_agentic_task.py +79 -0
  29. optexity/inference/core/interaction/handle_check.py +57 -0
  30. optexity/inference/core/interaction/handle_click.py +79 -0
  31. optexity/inference/core/interaction/handle_command.py +261 -0
  32. optexity/inference/core/interaction/handle_input.py +76 -0
  33. optexity/inference/core/interaction/handle_keypress.py +16 -0
  34. optexity/inference/core/interaction/handle_select.py +109 -0
  35. optexity/inference/core/interaction/handle_select_utils.py +132 -0
  36. optexity/inference/core/interaction/handle_upload.py +59 -0
  37. optexity/inference/core/interaction/utils.py +81 -0
  38. optexity/inference/core/logging.py +406 -0
  39. optexity/inference/core/run_assertion.py +55 -0
  40. optexity/inference/core/run_automation.py +463 -0
  41. optexity/inference/core/run_extraction.py +240 -0
  42. optexity/inference/core/run_interaction.py +254 -0
  43. optexity/inference/core/run_python_script.py +20 -0
  44. optexity/inference/core/run_two_fa.py +120 -0
  45. optexity/inference/core/two_factor_auth/__init__.py +0 -0
  46. optexity/inference/infra/__init__.py +0 -0
  47. optexity/inference/infra/browser.py +455 -0
  48. optexity/inference/infra/browser_extension.py +20 -0
  49. optexity/inference/models/__init__.py +22 -0
  50. optexity/inference/models/gemini.py +113 -0
  51. optexity/inference/models/human.py +20 -0
  52. optexity/inference/models/llm_model.py +210 -0
  53. optexity/inference/run_local.py +200 -0
  54. optexity/schema/__init__.py +0 -0
  55. optexity/schema/actions/__init__.py +0 -0
  56. optexity/schema/actions/assertion_action.py +66 -0
  57. optexity/schema/actions/extraction_action.py +143 -0
  58. optexity/schema/actions/interaction_action.py +330 -0
  59. optexity/schema/actions/misc_action.py +18 -0
  60. optexity/schema/actions/prompts.py +27 -0
  61. optexity/schema/actions/two_fa_action.py +24 -0
  62. optexity/schema/automation.py +432 -0
  63. optexity/schema/callback.py +16 -0
  64. optexity/schema/inference.py +87 -0
  65. optexity/schema/memory.py +100 -0
  66. optexity/schema/task.py +212 -0
  67. optexity/schema/token_usage.py +48 -0
  68. optexity/utils/__init__.py +0 -0
  69. optexity/utils/settings.py +54 -0
  70. optexity/utils/utils.py +76 -0
  71. {optexity-0.1.2.dist-info → optexity-0.1.4.dist-info}/METADATA +20 -36
  72. optexity-0.1.4.dist-info/RECORD +80 -0
  73. optexity-0.1.2.dist-info/RECORD +0 -11
  74. {optexity-0.1.2.dist-info → optexity-0.1.4.dist-info}/WHEEL +0 -0
  75. {optexity-0.1.2.dist-info → optexity-0.1.4.dist-info}/entry_points.txt +0 -0
  76. {optexity-0.1.2.dist-info → optexity-0.1.4.dist-info}/licenses/LICENSE +0 -0
  77. {optexity-0.1.2.dist-info → optexity-0.1.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,251 @@
1
+ import argparse
2
+ import asyncio
3
+ import logging
4
+ from contextlib import asynccontextmanager
5
+ from datetime import datetime, timedelta, timezone
6
+ from urllib.parse import urljoin
7
+
8
+ import httpx
9
+ from fastapi import Body, FastAPI
10
+ from fastapi.responses import JSONResponse
11
+ from pydantic import BaseModel
12
+ from uvicorn import run
13
+
14
+ from optexity.inference.core.run_automation import run_automation
15
+ from optexity.schema.inference import InferenceRequest
16
+ from optexity.schema.task import Task
17
+ from optexity.utils.settings import settings
18
+
19
+ logging.basicConfig(level=logging.INFO)
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class ChildProcessIdRequest(BaseModel):
24
+ new_child_process_id: str
25
+
26
+
27
+ child_process_id = None
28
+ task_running = False
29
+ last_task_start_time = None
30
+ task_queue: asyncio.Queue[Task] = asyncio.Queue()
31
+
32
+
33
+ async def task_processor():
34
+ """Background worker that processes tasks from the queue one at a time."""
35
+ global task_running
36
+ global last_task_start_time
37
+ logger.info("Task processor started")
38
+
39
+ while True:
40
+ try:
41
+ # Get next task from queue (blocks until one is available)
42
+ task = await task_queue.get()
43
+ task_running = True
44
+ last_task_start_time = datetime.now()
45
+ await run_automation(task, child_process_id)
46
+
47
+ except asyncio.CancelledError:
48
+ logger.info("Task processor cancelled")
49
+ break
50
+ except Exception as e:
51
+ logger.error(f"Error in task processor: {e}")
52
+ finally:
53
+
54
+ task_running = False
55
+
56
+
57
+ async def register_with_master():
58
+ """Register with master on startup (handles restarts automatically)."""
59
+ # Get my task metadata from ECS
60
+ async with httpx.AsyncClient(timeout=30.0) as client:
61
+ response = await client.get("http://169.254.170.2/v3/task")
62
+ response.raise_for_status()
63
+ metadata = response.json()
64
+
65
+ my_task_arn = metadata["TaskARN"]
66
+ my_ip = metadata["Containers"][0]["Networks"][0]["IPv4Addresses"][0]
67
+
68
+ my_port = None
69
+ for binding in metadata["Containers"][0].get("NetworkBindings", []):
70
+ if binding["containerPort"] == settings.CHILD_PORT_OFFSET:
71
+ my_port = binding["hostPort"]
72
+ break
73
+
74
+ if not my_port:
75
+ logger.error("Could not find host port binding")
76
+ raise ValueError("Host port not found in metadata")
77
+
78
+ # Register with master
79
+ async with httpx.AsyncClient(timeout=30.0) as client:
80
+ response = await client.post(
81
+ f"http://{settings.SERVER_URL}/register_child",
82
+ json={"task_arn": my_task_arn, "private_ip": my_ip, "port": my_port},
83
+ )
84
+ response.raise_for_status()
85
+
86
+ logger.info(f"Registered with master: {response.json()}")
87
+
88
+
89
+ def get_app_with_endpoints(is_aws: bool, child_id: int):
90
+ global child_process_id
91
+ child_process_id = child_id
92
+
93
+ @asynccontextmanager
94
+ async def lifespan(app: FastAPI):
95
+ """Lifespan context manager for startup and shutdown."""
96
+ # Startup
97
+
98
+ if is_aws:
99
+ asyncio.create_task(register_with_master())
100
+
101
+ logger.info("Registered with master")
102
+ asyncio.create_task(task_processor())
103
+ logger.info("Task processor background task started")
104
+ yield
105
+ # Shutdown (if needed in the future)
106
+ logger.info("Shutting down task processor")
107
+
108
+ app = FastAPI(title="Optexity Inference", lifespan=lifespan)
109
+
110
+ @app.get("/is_task_running", tags=["info"])
111
+ async def is_task_running():
112
+ """Is task running endpoint."""
113
+ return task_running
114
+
115
+ @app.get("/health", tags=["info"])
116
+ async def health():
117
+ """Health check endpoint."""
118
+ global last_task_start_time
119
+ if (
120
+ task_running
121
+ and last_task_start_time
122
+ and datetime.now() - last_task_start_time > timedelta(minutes=15)
123
+ ):
124
+ return JSONResponse(
125
+ status_code=503,
126
+ content={
127
+ "status": "unhealthy",
128
+ "message": "Task not finished in the last 15 minutes",
129
+ },
130
+ )
131
+ return JSONResponse(
132
+ status_code=200,
133
+ content={
134
+ "status": "healthy",
135
+ "task_running": task_running,
136
+ "queued_tasks": task_queue.qsize(),
137
+ },
138
+ )
139
+
140
+ @app.post("/set_child_process_id", tags=["info"])
141
+ async def set_child_process_id(request: ChildProcessIdRequest):
142
+ """Set child process id endpoint."""
143
+ global child_process_id
144
+ child_process_id = int(request.new_child_process_id)
145
+ return JSONResponse(
146
+ content={"success": True, "message": "Child process id has been set"},
147
+ status_code=200,
148
+ )
149
+
150
+ @app.post("/allocate_task")
151
+ async def allocate_task(task: Task = Body(...)):
152
+ """Get details of a specific task."""
153
+ try:
154
+
155
+ await task_queue.put(task)
156
+ return JSONResponse(
157
+ content={"success": True, "message": "Task has been allocated"},
158
+ status_code=202,
159
+ )
160
+ except Exception as e:
161
+ logger.error(f"Error allocating task {task.task_id}: {e}")
162
+ return JSONResponse(
163
+ content={"success": False, "message": str(e)}, status_code=500
164
+ )
165
+
166
+ if not is_aws:
167
+
168
+ @app.post("/inference")
169
+ async def inference(inference_request: InferenceRequest = Body(...)):
170
+ response_data: dict | None = None
171
+ try:
172
+
173
+ async with httpx.AsyncClient(timeout=30.0) as client:
174
+ url = urljoin(settings.SERVER_URL, settings.INFERENCE_ENDPOINT)
175
+ headers = {"x-api-key": settings.API_KEY}
176
+ response = await client.post(
177
+ url, json=inference_request.model_dump(), headers=headers
178
+ )
179
+ response_data = response.json()
180
+ response.raise_for_status()
181
+
182
+ task_data = response_data["task"]
183
+
184
+ task = Task.model_validate_json(task_data)
185
+ if task.use_proxy and settings.PROXY_URL is None:
186
+ raise ValueError(
187
+ "PROXY_URL is not set and is required when use_proxy is True"
188
+ )
189
+ task.allocated_at = datetime.now(timezone.utc)
190
+ await task_queue.put(task)
191
+
192
+ return JSONResponse(
193
+ content={
194
+ "success": True,
195
+ "message": "Task has been allocated",
196
+ "task_id": task.task_id,
197
+ },
198
+ status_code=202,
199
+ )
200
+
201
+ except Exception as e:
202
+ error = str(e)
203
+ if response_data is not None:
204
+ error = response_data.get("error", str(e))
205
+
206
+ logger.error(f"❌ Error fetching recordings: {error}")
207
+ return JSONResponse({"success": False, "error": error}, status_code=500)
208
+
209
+ return app
210
+
211
+
212
+ def main():
213
+ """Main function to run the server."""
214
+ parser = argparse.ArgumentParser(
215
+ description="Dynamic API endpoint generator for Optexity recordings"
216
+ )
217
+
218
+ parser.add_argument(
219
+ "--host",
220
+ type=str,
221
+ default="0.0.0.0",
222
+ help="Host to bind the server to (default: 0.0.0.0)",
223
+ )
224
+ parser.add_argument(
225
+ "--port",
226
+ type=int,
227
+ help="Port to run the server ",
228
+ )
229
+ parser.add_argument(
230
+ "--child_process_id",
231
+ type=int,
232
+ help="Child process ID",
233
+ )
234
+ parser.add_argument(
235
+ "--is_aws",
236
+ action="store_true",
237
+ help="Is child process",
238
+ default=False,
239
+ )
240
+
241
+ args = parser.parse_args()
242
+
243
+ app = get_app_with_endpoints(is_aws=args.is_aws, child_id=args.child_process_id)
244
+
245
+ # Start the server (this is blocking and manages its own event loop)
246
+ logger.info(f"Starting server on {args.host}:{args.port}")
247
+ run(app, host=args.host, port=args.port)
248
+
249
+
250
+ if __name__ == "__main__":
251
+ main()
File without changes
File without changes
@@ -0,0 +1,79 @@
1
+ import logging
2
+
3
+ from browser_use import Agent, BrowserSession, ChatGoogle, Tools
4
+
5
+ from optexity.inference.infra.browser import Browser
6
+ from optexity.schema.actions.interaction_action import (
7
+ AgenticTask,
8
+ CloseOverlayPopupAction,
9
+ )
10
+ from optexity.schema.memory import Memory
11
+ from optexity.schema.task import Task
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ async def handle_agentic_task(
17
+ agentic_task_action: AgenticTask | CloseOverlayPopupAction,
18
+ task: Task,
19
+ memory: Memory,
20
+ browser: Browser,
21
+ ):
22
+
23
+ if agentic_task_action.backend == "browser_use":
24
+
25
+ if isinstance(agentic_task_action, CloseOverlayPopupAction):
26
+ tools = Tools(
27
+ exclude_actions=[
28
+ "search",
29
+ "navigate",
30
+ "go_back",
31
+ "upload_file",
32
+ "scroll",
33
+ "find_text",
34
+ "send_keys",
35
+ "evaluate",
36
+ "switch",
37
+ "close",
38
+ "extract",
39
+ "dropdown_options",
40
+ "select_dropdown",
41
+ "write_file",
42
+ "read_file",
43
+ "replace_file",
44
+ ]
45
+ )
46
+ else:
47
+ tools = Tools()
48
+ llm = ChatGoogle(model="gemini-flash-latest")
49
+ browser_session = BrowserSession(
50
+ cdp_url=browser.cdp_url, keep_alive=agentic_task_action.keep_alive
51
+ )
52
+
53
+ step_directory = (
54
+ task.logs_directory / f"step_{str(memory.automation_state.step_index)}"
55
+ )
56
+ step_directory.mkdir(parents=True, exist_ok=True)
57
+
58
+ agent = Agent(
59
+ task=agentic_task_action.task,
60
+ llm=llm,
61
+ browser_session=browser_session,
62
+ use_vision=agentic_task_action.use_vision,
63
+ tools=tools,
64
+ calculate_cost=True,
65
+ save_conversation_path=step_directory,
66
+ )
67
+ logger.debug(f"Starting browser session for agentic task {browser.cdp_url} ")
68
+ await agent.browser_session.start()
69
+ logger.debug(f"Finally running agentic task on browser_use {browser.cdp_url} ")
70
+ await agent.run(max_steps=agentic_task_action.max_steps)
71
+ logger.debug(f"Agentic task completed on browser_use {browser.cdp_url} ")
72
+
73
+ agent.stop()
74
+ if agent.browser_session:
75
+ await agent.browser_session.stop()
76
+ await agent.browser_session.reset()
77
+
78
+ elif agentic_task_action.backend == "browserbase":
79
+ raise NotImplementedError("Browserbase is not supported yet")
@@ -0,0 +1,57 @@
1
+ import logging
2
+
3
+ from optexity.inference.core.interaction.handle_command import (
4
+ command_based_action_with_retry,
5
+ )
6
+ from optexity.inference.infra.browser import Browser
7
+ from optexity.schema.actions.interaction_action import CheckAction, UncheckAction
8
+ from optexity.schema.memory import Memory
9
+ from optexity.schema.task import Task
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ async def handle_check_element(
15
+ check_element_action: CheckAction,
16
+ task: Task,
17
+ memory: Memory,
18
+ browser: Browser,
19
+ max_timeout_seconds_per_try: float,
20
+ max_tries: int,
21
+ ):
22
+
23
+ if check_element_action.command and not check_element_action.skip_command:
24
+ last_error = await command_based_action_with_retry(
25
+ check_element_action,
26
+ browser,
27
+ memory,
28
+ task,
29
+ max_tries,
30
+ max_timeout_seconds_per_try,
31
+ )
32
+
33
+ if last_error is None:
34
+ return
35
+
36
+
37
+ async def handle_uncheck_element(
38
+ uncheck_element_action: UncheckAction,
39
+ task: Task,
40
+ memory: Memory,
41
+ browser: Browser,
42
+ max_timeout_seconds_per_try: float,
43
+ max_tries: int,
44
+ ):
45
+
46
+ if uncheck_element_action.command and not uncheck_element_action.skip_command:
47
+ last_error = await command_based_action_with_retry(
48
+ uncheck_element_action,
49
+ browser,
50
+ memory,
51
+ task,
52
+ max_tries,
53
+ max_timeout_seconds_per_try,
54
+ )
55
+
56
+ if last_error is None:
57
+ return
@@ -0,0 +1,79 @@
1
+ import logging
2
+
3
+ from optexity.inference.core.interaction.handle_command import (
4
+ command_based_action_with_retry,
5
+ )
6
+ from optexity.inference.core.interaction.utils import (
7
+ get_index_from_prompt,
8
+ handle_download,
9
+ )
10
+ from optexity.inference.infra.browser import Browser
11
+ from optexity.schema.actions.interaction_action import ClickElementAction
12
+ from optexity.schema.memory import Memory
13
+ from optexity.schema.task import Task
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ async def handle_click_element(
19
+ click_element_action: ClickElementAction,
20
+ task: Task,
21
+ memory: Memory,
22
+ browser: Browser,
23
+ max_timeout_seconds_per_try: float,
24
+ max_tries: int,
25
+ ):
26
+
27
+ if click_element_action.command and not click_element_action.skip_command:
28
+ last_error = await command_based_action_with_retry(
29
+ click_element_action,
30
+ browser,
31
+ memory,
32
+ task,
33
+ max_tries,
34
+ max_timeout_seconds_per_try,
35
+ )
36
+
37
+ if last_error is None:
38
+ return
39
+
40
+ if not click_element_action.skip_prompt:
41
+ logger.debug(
42
+ f"Executing prompt-based action: {click_element_action.__class__.__name__}"
43
+ )
44
+ await click_element_index(click_element_action, browser, memory, task)
45
+
46
+
47
+ async def click_element_index(
48
+ click_element_action: ClickElementAction,
49
+ browser: Browser,
50
+ memory: Memory,
51
+ task: Task,
52
+ ):
53
+
54
+ try:
55
+ index = await get_index_from_prompt(
56
+ memory, click_element_action.prompt_instructions, browser
57
+ )
58
+ if index is None:
59
+ return
60
+
61
+ async def _actual_click_element():
62
+ action_model = browser.backend_agent.ActionModel(
63
+ **{"click": {"index": index}}
64
+ )
65
+ await browser.backend_agent.multi_act([action_model])
66
+
67
+ if click_element_action.expect_download:
68
+ await handle_download(
69
+ _actual_click_element,
70
+ memory,
71
+ browser,
72
+ task,
73
+ click_element_action.download_filename,
74
+ )
75
+ else:
76
+ await _actual_click_element()
77
+ except Exception as e:
78
+ logger.error(f"Error in click_element_index: {e}")
79
+ return