diff --git a/client.py b/client.py index 3d371cb..9b686ac 100644 --- a/client.py +++ b/client.py @@ -1,7 +1,8 @@ import time +import os import httpx from datetime import datetime, timezone -from typing import Optional +from typing import Optional, Union from .types import Config, MaxunError @@ -11,7 +12,8 @@ def __init__(self, config: Config): headers = { "x-api-key": self.api_key, - "Content-Type": "application/json", + # Content-Type is intentionally omitted here so httpx can set it + # correctly per request (e.g. multipart/form-data for file uploads) } if config.team_id: @@ -161,6 +163,82 @@ async def extract_with_llm(self, options: dict): self.client.post("/extract/llm", json=options, timeout=300) ) + async def create_document_extract_robot( + self, + file: Union[str, bytes], + prompt: str, + robot_name: Optional[str] = None, + ollama_model: Optional[str] = None, + file_name: Optional[str] = None, + ) -> dict: + """Create a document-extraction robot from a PDF file path or bytes.""" + if isinstance(file, str): + file_name = file_name or os.path.basename(file) + with open(file, 'rb') as f: + file_bytes = f.read() + else: + file_bytes = file + file_name = file_name or 'document.pdf' + + data = {'prompt': prompt} + if robot_name: + data['robotName'] = robot_name + if ollama_model: + data['ollamaModel'] = ollama_model + + response = await self.client.post( + '/robots/document', + files={'file': (file_name, file_bytes, 'application/pdf')}, + data=data, + timeout=120, + ) + response.raise_for_status() + body = response.json() + if not body.get('data') and not body.get('robot'): + raise MaxunError('Failed to create document robot') + return body + + async def create_document_parse_robot( + self, + file: Union[str, bytes], + output_formats: list, + robot_name: Optional[str] = None, + file_name: Optional[str] = None, + ) -> dict: + """Create a document-parse robot from a PDF file path or bytes.""" + if isinstance(file, str): + file_name = file_name or os.path.basename(file) + with open(file, 'rb') as f: + file_bytes = f.read() + else: + file_bytes = file + file_name = file_name or 'document.pdf' + + valid_formats = {'markdown', 'html', 'links'} + filtered = [f for f in output_formats if f in valid_formats] + if not filtered: + raise MaxunError('At least one valid output format is required (markdown, html, links)') + + data = {} + if robot_name: + data['robotName'] = robot_name + + files_payload = [('file', (file_name, file_bytes, 'application/pdf'))] + for fmt in filtered: + files_payload.append(('outputFormats[]', (None, fmt))) + + response = await self.client.post( + '/robots/document-parse', + files=files_payload, + data=data, + timeout=120, + ) + response.raise_for_status() + body = response.json() + if not body.get('data') and not body.get('robot'): + raise MaxunError('Failed to create document-parse robot') + return body + async def create_crawl_robot(self, url: str, options: dict): return await self._handle( self.client.post("/crawl", json={"url": url, **options}) diff --git a/pyproject.toml b/pyproject.toml index 448dbf7..ef8b2dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "maxun" -version = "0.0.4" +version = "0.0.7" description = "Python SDK for Maxun - web automation and data extraction" requires-python = ">=3.8" license = { text = "MIT" } diff --git a/scrape.py b/scrape.py index 031b190..a80f70c 100644 --- a/scrape.py +++ b/scrape.py @@ -16,7 +16,7 @@ async def create( name: str, url: str, formats: Optional[List[Format]] = None, - prompt_instructions: Optional[str] = None, + smart_queries: Optional[str] = None, ) -> Robot: """ Create a scrape robot. @@ -24,9 +24,8 @@ async def create( :param name: Robot name. :param url: URL to scrape. :param formats: Output formats (default: ["markdown"]). - :param prompt_instructions: Optional Smart Queries prompt. After scraping the - LLM analyzes the page and returns an answer. Adds 2 extra credits per run - on top of the base 1 scrape credit. + :param smart_queries: Optional Smart Queries prompt. After scraping the + LLM analyzes the page and returns an answer. """ if not url: raise ValueError("URL is required") @@ -40,8 +39,8 @@ async def create( "url": url, "formats": formats or ["markdown"], } - if prompt_instructions: - meta["promptInstructions"] = prompt_instructions.strip() + if smart_queries: + meta["smartQueries"] = smart_queries.strip() workflow_file: WorkflowFile = { "meta": meta, diff --git a/types.py b/types.py index 644b353..ae3e2ce 100644 --- a/types.py +++ b/types.py @@ -7,7 +7,7 @@ RobotType = Literal["extract", "scrape", "crawl", "search"] RobotMode = Literal["normal", "bulk"] -Format = Literal["markdown", "html", "screenshot-visible", "screenshot-fullpage"] +Format = Literal["markdown", "html", "text", "links", "screenshot-visible", "screenshot-fullpage"] RunStatus = Literal["running", "queued", "success", "failed", "aborting", "aborted"] TimeUnit = Literal["MINUTES", "HOURS", "DAYS", "WEEKS", "MONTHS"] CrawlMode = Literal["domain", "subdomain", "path"]