From d70c91bca2937c430dd74e10d298521dc326d8a2 Mon Sep 17 00:00:00 2001 From: Shuchang Zheng Date: Sun, 18 May 2025 12:43:22 -0700 Subject: [PATCH] reorder api endpoints (#2377) --- skyvern/forge/sdk/routes/agent_protocol.py | 980 ++++++++++--------- skyvern/forge/sdk/routes/browser_sessions.py | 104 +- skyvern/forge/sdk/routes/credentials.py | 356 +++---- 3 files changed, 721 insertions(+), 719 deletions(-) diff --git a/skyvern/forge/sdk/routes/agent_protocol.py b/skyvern/forge/sdk/routes/agent_protocol.py index 77903e6f..bb51b3a9 100644 --- a/skyvern/forge/sdk/routes/agent_protocol.py +++ b/skyvern/forge/sdk/routes/agent_protocol.py @@ -99,6 +99,497 @@ class AISuggestionType(str, Enum): DATA_SCHEMA = "data_schema" +################# /v1 Endpoints ################# +@base_router.post( + "/run/tasks", + tags=["Agent"], + openapi_extra={ + "x-fern-sdk-group-name": "agent", + "x-fern-sdk-method-name": "run_task", + "x-fern-examples": [ + { + "code-samples": [ + { + "sdk": "python", + "code": RUN_TASK_CODE_SAMPLE, + } + ] + } + ], + }, + description="Run a task", + summary="Run a task", + responses={ + 200: {"description": "Successfully run task"}, + 400: {"description": "Invalid agent engine"}, + }, +) +@base_router.post("/run/tasks/", include_in_schema=False) +async def run_task( + request: Request, + background_tasks: BackgroundTasks, + run_request: TaskRunRequest, + current_org: Organization = Depends(org_auth_service.get_current_org), + x_api_key: Annotated[str | None, Header()] = None, + x_user_agent: Annotated[str | None, Header()] = None, +) -> TaskRunResponse: + analytics.capture("skyvern-oss-run-task", data={"url": run_request.url}) + await PermissionCheckerFactory.get_instance().check(current_org, browser_session_id=run_request.browser_session_id) + + if run_request.engine in CUA_ENGINES or run_request.engine == RunEngine.skyvern_v1: + # create task v1 + # if there's no url, call task generation first to generate the url, data schema if any + url = run_request.url + data_extraction_goal = None + data_extraction_schema = run_request.data_extraction_schema + navigation_goal = run_request.prompt + navigation_payload = None + task_generation = await task_v1_service.generate_task( + user_prompt=run_request.prompt, + organization=current_org, + ) + url = url or task_generation.url + navigation_goal = task_generation.navigation_goal or run_request.prompt + if run_request.engine in CUA_ENGINES: + navigation_goal = run_request.prompt + navigation_payload = task_generation.navigation_payload + data_extraction_goal = task_generation.data_extraction_goal + data_extraction_schema = data_extraction_schema or task_generation.extracted_information_schema + + task_v1_request = TaskRequest( + title=run_request.title, + url=url, + navigation_goal=navigation_goal, + navigation_payload=navigation_payload, + data_extraction_goal=data_extraction_goal, + extracted_information_schema=data_extraction_schema, + error_code_mapping=run_request.error_code_mapping, + proxy_location=run_request.proxy_location, + browser_session_id=run_request.browser_session_id, + totp_verification_url=run_request.totp_url, + totp_identifier=run_request.totp_identifier, + include_action_history_in_verification=run_request.include_action_history_in_verification, + ) + task_v1_response = await task_v1_service.run_task( + task=task_v1_request, + organization=current_org, + engine=run_request.engine, + x_max_steps_override=run_request.max_steps, + x_api_key=x_api_key, + request=request, + background_tasks=background_tasks, + ) + run_type = RunType.task_v1 + if run_request.engine == RunEngine.openai_cua: + run_type = RunType.openai_cua + elif run_request.engine == RunEngine.anthropic_cua: + run_type = RunType.anthropic_cua + # build the task run response + return TaskRunResponse( + run_id=task_v1_response.task_id, + run_type=run_type, + status=str(task_v1_response.status), + output=task_v1_response.extracted_information, + failure_reason=task_v1_response.failure_reason, + created_at=task_v1_response.created_at, + modified_at=task_v1_response.modified_at, + app_url=f"{settings.SKYVERN_APP_URL.rstrip('/')}/tasks/{task_v1_response.task_id}", + run_request=TaskRunRequest( + engine=run_request.engine, + prompt=task_v1_response.navigation_goal, + url=task_v1_response.url, + webhook_url=task_v1_response.webhook_callback_url, + totp_identifier=task_v1_response.totp_identifier, + totp_url=task_v1_response.totp_verification_url, + proxy_location=task_v1_response.proxy_location, + max_steps=task_v1_response.max_steps_per_run, + data_extraction_schema=task_v1_response.extracted_information_schema, + error_code_mapping=task_v1_response.error_code_mapping, + browser_session_id=run_request.browser_session_id, + ), + ) + if run_request.engine == RunEngine.skyvern_v2: + # create task v2 + try: + task_v2 = await task_v2_service.initialize_task_v2( + organization=current_org, + user_prompt=run_request.prompt, + user_url=run_request.url, + totp_identifier=run_request.totp_identifier, + totp_verification_url=run_request.totp_url, + webhook_callback_url=run_request.webhook_url, + proxy_location=run_request.proxy_location, + publish_workflow=run_request.publish_workflow, + extracted_information_schema=run_request.data_extraction_schema, + error_code_mapping=run_request.error_code_mapping, + create_task_run=True, + ) + except LLMProviderError: + LOG.error("LLM failure to initialize task v2", exc_info=True) + raise HTTPException( + status_code=500, detail="Skyvern LLM failure to initialize task v2. Please try again later." + ) + await AsyncExecutorFactory.get_executor().execute_task_v2( + request=request, + background_tasks=background_tasks, + organization_id=current_org.organization_id, + task_v2_id=task_v2.observer_cruise_id, + max_steps_override=run_request.max_steps, + browser_session_id=run_request.browser_session_id, + ) + refreshed_task_v2 = await app.DATABASE.get_task_v2( + task_v2_id=task_v2.observer_cruise_id, organization_id=current_org.organization_id + ) + task_v2 = refreshed_task_v2 if refreshed_task_v2 else task_v2 + return TaskRunResponse( + run_id=task_v2.observer_cruise_id, + run_type=RunType.task_v2, + status=str(task_v2.status), + output=task_v2.output, + failure_reason=None, + created_at=task_v2.created_at, + modified_at=task_v2.modified_at, + app_url=f"{settings.SKYVERN_APP_URL.rstrip('/')}/workflows/{task_v2.workflow_permanent_id}/{task_v2.workflow_run_id}", + run_request=TaskRunRequest( + engine=RunEngine.skyvern_v2, + prompt=task_v2.prompt, + url=task_v2.url, + webhook_url=task_v2.webhook_callback_url, + totp_identifier=task_v2.totp_identifier, + totp_url=task_v2.totp_verification_url, + proxy_location=task_v2.proxy_location, + max_steps=run_request.max_steps, + browser_session_id=run_request.browser_session_id, + error_code_mapping=task_v2.error_code_mapping, + data_extraction_schema=task_v2.extracted_information_schema, + publish_workflow=run_request.publish_workflow, + ), + ) + LOG.error("Invalid agent engine", engine=run_request.engine, organization_id=current_org.organization_id) + raise HTTPException(status_code=400, detail=f"Invalid agent engine: {run_request.engine}") + + +@base_router.post( + "/run/workflows", + tags=["Agent"], + openapi_extra={ + "x-fern-sdk-group-name": "agent", + "x-fern-sdk-method-name": "run_workflow", + "x-fern-examples": [ + { + "code-samples": [ + { + "sdk": "python", + "code": RUN_WORKFLOW_CODE_SAMPLE, + } + ] + } + ], + }, + description="Run a workflow", + summary="Run a workflow", + responses={ + 200: {"description": "Successfully run workflow"}, + 400: {"description": "Invalid workflow run request"}, + }, +) +@base_router.post("/run/workflows/", include_in_schema=False) +async def run_workflow( + request: Request, + background_tasks: BackgroundTasks, + workflow_run_request: WorkflowRunRequest, + current_org: Organization = Depends(org_auth_service.get_current_org), + template: bool = Query(False), + x_api_key: Annotated[str | None, Header()] = None, + x_max_steps_override: Annotated[int | None, Header()] = None, + x_user_agent: Annotated[str | None, Header()] = None, +) -> WorkflowRunResponse: + analytics.capture("skyvern-oss-run-workflow") + await PermissionCheckerFactory.get_instance().check( + current_org, browser_session_id=workflow_run_request.browser_session_id + ) + workflow_id = workflow_run_request.workflow_id + context = skyvern_context.ensure_context() + request_id = context.request_id + legacy_workflow_request = WorkflowRequestBody( + data=workflow_run_request.parameters, + proxy_location=workflow_run_request.proxy_location, + webhook_callback_url=workflow_run_request.webhook_url, + totp_identifier=workflow_run_request.totp_identifier, + totp_url=workflow_run_request.totp_url, + browser_session_id=workflow_run_request.browser_session_id, + ) + workflow_run = await workflow_service.run_workflow( + workflow_id=workflow_id, + organization=current_org, + workflow_request=legacy_workflow_request, + template=template, + version=None, + max_steps=x_max_steps_override, + api_key=x_api_key, + request_id=request_id, + request=request, + background_tasks=background_tasks, + ) + + return WorkflowRunResponse( + run_id=workflow_run.workflow_run_id, + run_type=RunType.workflow_run, + status=str(workflow_run.status), + output=None, + failure_reason=workflow_run.failure_reason, + created_at=workflow_run.created_at, + modified_at=workflow_run.modified_at, + run_request=workflow_run_request, + downloaded_files=None, + recording_url=None, + app_url=f"{settings.SKYVERN_APP_URL.rstrip('/')}/workflows/{workflow_run.workflow_permanent_id}/{workflow_run.workflow_run_id}", + ) + + +@base_router.get( + "/runs/{run_id}", + tags=["Agent"], + response_model=RunResponse, + description="Get run information (task run, workflow run)", + summary="Get a task or a workflow run by id", + openapi_extra={ + "x-fern-sdk-group-name": "agent", + "x-fern-sdk-method-name": "get_run", + "x-fern-examples": [{"code-samples": [{"sdk": "python", "code": GET_RUN_CODE_SAMPLE}]}], + }, + responses={ + 200: {"description": "Successfully got run"}, + 404: {"description": "Run not found"}, + }, +) +@base_router.get( + "/runs/{run_id}/", + response_model=RunResponse, + include_in_schema=False, +) +async def get_run( + run_id: str = Path( + ..., description="The id of the task run or the workflow run.", examples=["tsk_123", "tsk_v2_123", "wr_123"] + ), + current_org: Organization = Depends(org_auth_service.get_current_org), +) -> RunResponse: + run_response = await run_service.get_run_response(run_id, organization_id=current_org.organization_id) + if not run_response: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Task run not found {run_id}", + ) + return run_response + + +@base_router.post( + "/runs/{run_id}/cancel", + tags=["Agent"], + openapi_extra={ + "x-fern-sdk-group-name": "agent", + "x-fern-sdk-method-name": "cancel_run", + }, + description="Cancel a run (task or workflow)", + summary="Cancel a task or workflow run", +) +@base_router.post("/runs/{run_id}/cancel/", include_in_schema=False) +async def cancel_run( + run_id: str = Path(..., description="The id of the task run or the workflow run to cancel."), + current_org: Organization = Depends(org_auth_service.get_current_org), + x_api_key: Annotated[str | None, Header()] = None, +) -> None: + analytics.capture("skyvern-oss-agent-cancel-run") + + await run_service.cancel_run(run_id, organization_id=current_org.organization_id, api_key=x_api_key) + + +@legacy_base_router.post( + "/workflows", + openapi_extra={ + "requestBody": { + "content": {"application/x-yaml": {"schema": WorkflowCreateYAMLRequest.model_json_schema()}}, + "required": True, + }, + "x-fern-sdk-group-name": "agent", + "x-fern-sdk-method-name": "create_workflow", + }, + response_model=Workflow, + tags=["agent"], +) +@legacy_base_router.post( + "/workflows/", + openapi_extra={ + "requestBody": { + "content": {"application/x-yaml": {"schema": WorkflowCreateYAMLRequest.model_json_schema()}}, + "required": True, + }, + }, + response_model=Workflow, + include_in_schema=False, +) +@base_router.post( + "/workflows", + response_model=Workflow, + tags=["Agent"], + openapi_extra={ + "x-fern-sdk-group-name": "agent", + "x-fern-sdk-method-name": "create_workflow", + }, + description="Create a new workflow", + summary="Create a new workflow", + responses={ + 200: {"description": "Successfully created workflow"}, + 422: {"description": "Invalid workflow definition"}, + }, +) +@base_router.post( + "/workflows/", + response_model=Workflow, + include_in_schema=False, +) +async def create_workflow( + request: Request, + current_org: Organization = Depends(org_auth_service.get_current_org), +) -> Workflow: + analytics.capture("skyvern-oss-agent-workflow-create") + raw_yaml = await request.body() + try: + workflow_yaml = yaml.safe_load(raw_yaml) + except yaml.YAMLError: + raise HTTPException(status_code=422, detail="Invalid YAML") + + try: + workflow_create_request = WorkflowCreateYAMLRequest.model_validate(workflow_yaml) + return await app.WORKFLOW_SERVICE.create_workflow_from_request( + organization=current_org, request=workflow_create_request + ) + except WorkflowParameterMissingRequiredValue as e: + raise e + except Exception as e: + LOG.error("Failed to create workflow", exc_info=True, organization_id=current_org.organization_id) + raise FailedToCreateWorkflow(str(e)) + + +@legacy_base_router.put( + "/workflows/{workflow_id}", + openapi_extra={ + "requestBody": { + "content": {"application/x-yaml": {"schema": WorkflowCreateYAMLRequest.model_json_schema()}}, + "required": True, + }, + "x-fern-sdk-group-name": "agent", + "x-fern-sdk-method-name": "update_workflow", + }, + response_model=Workflow, + tags=["agent"], +) +@legacy_base_router.put( + "/workflows/{workflow_id}/", + openapi_extra={ + "requestBody": { + "content": {"application/x-yaml": {"schema": WorkflowCreateYAMLRequest.model_json_schema()}}, + "required": True, + }, + }, + response_model=Workflow, + include_in_schema=False, +) +@base_router.post( + "/workflows/{workflow_id}", + response_model=Workflow, + tags=["Agent"], + openapi_extra={ + "requestBody": { + "content": {"application/x-yaml": {"schema": WorkflowCreateYAMLRequest.model_json_schema()}}, + "required": True, + }, + "x-fern-sdk-group-name": "agent", + "x-fern-sdk-method-name": "update_workflow", + }, + description="Update a workflow definition", + summary="Update a workflow definition", + responses={ + 200: {"description": "Successfully updated workflow"}, + 422: {"description": "Invalid workflow definition"}, + }, +) +@base_router.post( + "/workflows/{workflow_id}/", + openapi_extra={ + "requestBody": { + "content": {"application/x-yaml": {"schema": WorkflowCreateYAMLRequest.model_json_schema()}}, + "required": True, + }, + }, + response_model=Workflow, + include_in_schema=False, +) +async def update_workflow( + request: Request, + workflow_id: str = Path( + ..., description="The ID of the workflow to update. Workflow ID starts with `wpid_`.", examples=["wpid_123"] + ), + current_org: Organization = Depends(org_auth_service.get_current_org), +) -> Workflow: + analytics.capture("skyvern-oss-agent-workflow-update") + # validate the workflow + raw_yaml = await request.body() + try: + workflow_yaml = yaml.safe_load(raw_yaml) + except yaml.YAMLError: + raise HTTPException(status_code=422, detail="Invalid YAML") + + try: + workflow_create_request = WorkflowCreateYAMLRequest.model_validate(workflow_yaml) + return await app.WORKFLOW_SERVICE.create_workflow_from_request( + organization=current_org, + request=workflow_create_request, + workflow_permanent_id=workflow_id, + ) + except WorkflowParameterMissingRequiredValue as e: + raise e + except Exception as e: + LOG.exception( + "Failed to update workflow", + workflow_permanent_id=workflow_id, + organization_id=current_org.organization_id, + ) + raise FailedToUpdateWorkflow(workflow_id, f"<{type(e).__name__}: {str(e)}>") + + +@legacy_base_router.delete( + "/workflows/{workflow_id}", + tags=["agent"], + openapi_extra={ + "x-fern-sdk-group-name": "agent", + "x-fern-sdk-method-name": "delete_workflow", + }, +) +@legacy_base_router.delete("/workflows/{workflow_id}/", include_in_schema=False) +@base_router.post( + "/workflows/{workflow_id}/delete", + tags=["Agent"], + openapi_extra={ + "x-fern-sdk-group-name": "agent", + "x-fern-sdk-method-name": "delete_workflow", + }, + description="Delete a workflow", + summary="Delete a workflow", + responses={200: {"description": "Successfully deleted workflow"}}, +) +@base_router.post("/workflows/{workflow_id}/delete/", include_in_schema=False) +async def delete_workflow( + workflow_id: str = Path( + ..., description="The ID of the workflow to delete. Workflow ID starts with `wpid_`.", examples=["wpid_123"] + ), + current_org: Organization = Depends(org_auth_service.get_current_org), +) -> None: + analytics.capture("skyvern-oss-agent-workflow-delete") + await app.WORKFLOW_SERVICE.delete_workflow_by_permanent_id(workflow_id, current_org.organization_id) + + +################# Legacy Endpoints ################# @legacy_base_router.post( "/webhook", tags=["server"], @@ -409,42 +900,6 @@ async def get_runs( return ORJSONResponse([run.model_dump() for run in runs]) -@base_router.get( - "/runs/{run_id}", - tags=["Agent"], - response_model=RunResponse, - description="Get a task or a workflow run by id", - summary="Get a task or a workflow run by id", - openapi_extra={ - "x-fern-sdk-group-name": "agent", - "x-fern-sdk-method-name": "get_run", - "x-fern-examples": [{"code-samples": [{"sdk": "python", "code": GET_RUN_CODE_SAMPLE}]}], - }, - responses={ - 200: {"description": "Successfully got run"}, - 404: {"description": "Run not found"}, - }, -) -@base_router.get( - "/runs/{run_id}/", - response_model=RunResponse, - include_in_schema=False, -) -async def get_run( - run_id: str = Path( - ..., description="The id of the task run or the workflow run.", examples=["tsk_123", "tsk_v2_123", "wr_123"] - ), - current_org: Organization = Depends(org_auth_service.get_current_org), -) -> RunResponse: - run_response = await run_service.get_run_response(run_id, organization_id=current_org.organization_id) - if not run_response: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Task run not found {run_id}", - ) - return run_response - - @legacy_base_router.get( "/tasks/{task_id}/steps", tags=["agent"], @@ -798,191 +1253,6 @@ async def get_workflow_run( ) -@legacy_base_router.post( - "/workflows", - openapi_extra={ - "requestBody": { - "content": {"application/x-yaml": {"schema": WorkflowCreateYAMLRequest.model_json_schema()}}, - "required": True, - }, - "x-fern-sdk-group-name": "agent", - "x-fern-sdk-method-name": "create_workflow", - }, - response_model=Workflow, - tags=["agent"], -) -@legacy_base_router.post( - "/workflows/", - openapi_extra={ - "requestBody": { - "content": {"application/x-yaml": {"schema": WorkflowCreateYAMLRequest.model_json_schema()}}, - "required": True, - }, - }, - response_model=Workflow, - include_in_schema=False, -) -@base_router.post( - "/workflows", - response_model=Workflow, - tags=["Agent"], - openapi_extra={ - "x-fern-sdk-group-name": "agent", - "x-fern-sdk-method-name": "create_workflow", - }, - description="Create a new workflow", - summary="Create a new workflow", - responses={ - 200: {"description": "Successfully created workflow"}, - 422: {"description": "Invalid workflow definition"}, - }, -) -@base_router.post( - "/workflows/", - response_model=Workflow, - include_in_schema=False, -) -async def create_workflow( - request: Request, - current_org: Organization = Depends(org_auth_service.get_current_org), -) -> Workflow: - analytics.capture("skyvern-oss-agent-workflow-create") - raw_yaml = await request.body() - try: - workflow_yaml = yaml.safe_load(raw_yaml) - except yaml.YAMLError: - raise HTTPException(status_code=422, detail="Invalid YAML") - - try: - workflow_create_request = WorkflowCreateYAMLRequest.model_validate(workflow_yaml) - return await app.WORKFLOW_SERVICE.create_workflow_from_request( - organization=current_org, request=workflow_create_request - ) - except WorkflowParameterMissingRequiredValue as e: - raise e - except Exception as e: - LOG.error("Failed to create workflow", exc_info=True, organization_id=current_org.organization_id) - raise FailedToCreateWorkflow(str(e)) - - -@legacy_base_router.put( - "/workflows/{workflow_id}", - openapi_extra={ - "requestBody": { - "content": {"application/x-yaml": {"schema": WorkflowCreateYAMLRequest.model_json_schema()}}, - "required": True, - }, - "x-fern-sdk-group-name": "agent", - "x-fern-sdk-method-name": "update_workflow", - }, - response_model=Workflow, - tags=["agent"], -) -@legacy_base_router.put( - "/workflows/{workflow_id}/", - openapi_extra={ - "requestBody": { - "content": {"application/x-yaml": {"schema": WorkflowCreateYAMLRequest.model_json_schema()}}, - "required": True, - }, - }, - response_model=Workflow, - include_in_schema=False, -) -@base_router.post( - "/workflows/{workflow_id}", - response_model=Workflow, - tags=["Agent"], - openapi_extra={ - "requestBody": { - "content": {"application/x-yaml": {"schema": WorkflowCreateYAMLRequest.model_json_schema()}}, - "required": True, - }, - "x-fern-sdk-group-name": "agent", - "x-fern-sdk-method-name": "update_workflow", - }, - description="Update a workflow definition", - summary="Update a workflow definition", - responses={ - 200: {"description": "Successfully updated workflow"}, - 422: {"description": "Invalid workflow definition"}, - }, -) -@base_router.post( - "/workflows/{workflow_id}/", - openapi_extra={ - "requestBody": { - "content": {"application/x-yaml": {"schema": WorkflowCreateYAMLRequest.model_json_schema()}}, - "required": True, - }, - }, - response_model=Workflow, - include_in_schema=False, -) -async def update_workflow( - request: Request, - workflow_id: str = Path( - ..., description="The ID of the workflow to update. Workflow ID starts with `wpid_`.", examples=["wpid_123"] - ), - current_org: Organization = Depends(org_auth_service.get_current_org), -) -> Workflow: - analytics.capture("skyvern-oss-agent-workflow-update") - # validate the workflow - raw_yaml = await request.body() - try: - workflow_yaml = yaml.safe_load(raw_yaml) - except yaml.YAMLError: - raise HTTPException(status_code=422, detail="Invalid YAML") - - try: - workflow_create_request = WorkflowCreateYAMLRequest.model_validate(workflow_yaml) - return await app.WORKFLOW_SERVICE.create_workflow_from_request( - organization=current_org, - request=workflow_create_request, - workflow_permanent_id=workflow_id, - ) - except WorkflowParameterMissingRequiredValue as e: - raise e - except Exception as e: - LOG.exception( - "Failed to update workflow", - workflow_permanent_id=workflow_id, - organization_id=current_org.organization_id, - ) - raise FailedToUpdateWorkflow(workflow_id, f"<{type(e).__name__}: {str(e)}>") - - -@legacy_base_router.delete( - "/workflows/{workflow_id}", - tags=["agent"], - openapi_extra={ - "x-fern-sdk-group-name": "agent", - "x-fern-sdk-method-name": "delete_workflow", - }, -) -@legacy_base_router.delete("/workflows/{workflow_id}/", include_in_schema=False) -@base_router.post( - "/workflows/{workflow_id}/delete", - tags=["Agent"], - openapi_extra={ - "x-fern-sdk-group-name": "agent", - "x-fern-sdk-method-name": "delete_workflow", - }, - description="Delete a workflow", - summary="Delete a workflow", - responses={200: {"description": "Successfully deleted workflow"}}, -) -@base_router.post("/workflows/{workflow_id}/delete/", include_in_schema=False) -async def delete_workflow( - workflow_id: str = Path( - ..., description="The ID of the workflow to delete. Workflow ID starts with `wpid_`.", examples=["wpid_123"] - ), - current_org: Organization = Depends(org_auth_service.get_current_org), -) -> None: - analytics.capture("skyvern-oss-agent-workflow-delete") - await app.WORKFLOW_SERVICE.delete_workflow_by_permanent_id(workflow_id, current_org.organization_id) - - @legacy_base_router.get( "/workflows", response_model=list[Workflow], @@ -1417,271 +1687,3 @@ async def _flatten_workflow_run_timeline(organization_id: str, workflow_run_id: final_workflow_run_block_timeline.extend(thought_timeline) final_workflow_run_block_timeline.sort(key=lambda x: x.created_at, reverse=True) return final_workflow_run_block_timeline - - -@base_router.post( - "/run/tasks", - tags=["Agent"], - openapi_extra={ - "x-fern-sdk-group-name": "agent", - "x-fern-sdk-method-name": "run_task", - "x-fern-examples": [ - { - "code-samples": [ - { - "sdk": "python", - "code": RUN_TASK_CODE_SAMPLE, - } - ] - } - ], - }, - description="Run a task", - summary="Run a task", - responses={ - 200: {"description": "Successfully run task"}, - 400: {"description": "Invalid agent engine"}, - }, -) -@base_router.post("/run/tasks/", include_in_schema=False) -async def run_task( - request: Request, - background_tasks: BackgroundTasks, - run_request: TaskRunRequest, - current_org: Organization = Depends(org_auth_service.get_current_org), - x_api_key: Annotated[str | None, Header()] = None, - x_user_agent: Annotated[str | None, Header()] = None, -) -> TaskRunResponse: - analytics.capture("skyvern-oss-run-task", data={"url": run_request.url}) - await PermissionCheckerFactory.get_instance().check(current_org, browser_session_id=run_request.browser_session_id) - - if run_request.engine in CUA_ENGINES or run_request.engine == RunEngine.skyvern_v1: - # create task v1 - # if there's no url, call task generation first to generate the url, data schema if any - url = run_request.url - data_extraction_goal = None - data_extraction_schema = run_request.data_extraction_schema - navigation_goal = run_request.prompt - navigation_payload = None - task_generation = await task_v1_service.generate_task( - user_prompt=run_request.prompt, - organization=current_org, - ) - url = url or task_generation.url - navigation_goal = task_generation.navigation_goal or run_request.prompt - if run_request.engine in CUA_ENGINES: - navigation_goal = run_request.prompt - navigation_payload = task_generation.navigation_payload - data_extraction_goal = task_generation.data_extraction_goal - data_extraction_schema = data_extraction_schema or task_generation.extracted_information_schema - - task_v1_request = TaskRequest( - title=run_request.title, - url=url, - navigation_goal=navigation_goal, - navigation_payload=navigation_payload, - data_extraction_goal=data_extraction_goal, - extracted_information_schema=data_extraction_schema, - error_code_mapping=run_request.error_code_mapping, - proxy_location=run_request.proxy_location, - browser_session_id=run_request.browser_session_id, - totp_verification_url=run_request.totp_url, - totp_identifier=run_request.totp_identifier, - include_action_history_in_verification=run_request.include_action_history_in_verification, - ) - task_v1_response = await task_v1_service.run_task( - task=task_v1_request, - organization=current_org, - engine=run_request.engine, - x_max_steps_override=run_request.max_steps, - x_api_key=x_api_key, - request=request, - background_tasks=background_tasks, - ) - run_type = RunType.task_v1 - if run_request.engine == RunEngine.openai_cua: - run_type = RunType.openai_cua - elif run_request.engine == RunEngine.anthropic_cua: - run_type = RunType.anthropic_cua - # build the task run response - return TaskRunResponse( - run_id=task_v1_response.task_id, - run_type=run_type, - status=str(task_v1_response.status), - output=task_v1_response.extracted_information, - failure_reason=task_v1_response.failure_reason, - created_at=task_v1_response.created_at, - modified_at=task_v1_response.modified_at, - app_url=f"{settings.SKYVERN_APP_URL.rstrip('/')}/tasks/{task_v1_response.task_id}", - run_request=TaskRunRequest( - engine=run_request.engine, - prompt=task_v1_response.navigation_goal, - url=task_v1_response.url, - webhook_url=task_v1_response.webhook_callback_url, - totp_identifier=task_v1_response.totp_identifier, - totp_url=task_v1_response.totp_verification_url, - proxy_location=task_v1_response.proxy_location, - max_steps=task_v1_response.max_steps_per_run, - data_extraction_schema=task_v1_response.extracted_information_schema, - error_code_mapping=task_v1_response.error_code_mapping, - browser_session_id=run_request.browser_session_id, - ), - ) - if run_request.engine == RunEngine.skyvern_v2: - # create task v2 - try: - task_v2 = await task_v2_service.initialize_task_v2( - organization=current_org, - user_prompt=run_request.prompt, - user_url=run_request.url, - totp_identifier=run_request.totp_identifier, - totp_verification_url=run_request.totp_url, - webhook_callback_url=run_request.webhook_url, - proxy_location=run_request.proxy_location, - publish_workflow=run_request.publish_workflow, - extracted_information_schema=run_request.data_extraction_schema, - error_code_mapping=run_request.error_code_mapping, - create_task_run=True, - ) - except LLMProviderError: - LOG.error("LLM failure to initialize task v2", exc_info=True) - raise HTTPException( - status_code=500, detail="Skyvern LLM failure to initialize task v2. Please try again later." - ) - await AsyncExecutorFactory.get_executor().execute_task_v2( - request=request, - background_tasks=background_tasks, - organization_id=current_org.organization_id, - task_v2_id=task_v2.observer_cruise_id, - max_steps_override=run_request.max_steps, - browser_session_id=run_request.browser_session_id, - ) - refreshed_task_v2 = await app.DATABASE.get_task_v2( - task_v2_id=task_v2.observer_cruise_id, organization_id=current_org.organization_id - ) - task_v2 = refreshed_task_v2 if refreshed_task_v2 else task_v2 - return TaskRunResponse( - run_id=task_v2.observer_cruise_id, - run_type=RunType.task_v2, - status=str(task_v2.status), - output=task_v2.output, - failure_reason=None, - created_at=task_v2.created_at, - modified_at=task_v2.modified_at, - app_url=f"{settings.SKYVERN_APP_URL.rstrip('/')}/workflows/{task_v2.workflow_permanent_id}/{task_v2.workflow_run_id}", - run_request=TaskRunRequest( - engine=RunEngine.skyvern_v2, - prompt=task_v2.prompt, - url=task_v2.url, - webhook_url=task_v2.webhook_callback_url, - totp_identifier=task_v2.totp_identifier, - totp_url=task_v2.totp_verification_url, - proxy_location=task_v2.proxy_location, - max_steps=run_request.max_steps, - browser_session_id=run_request.browser_session_id, - error_code_mapping=task_v2.error_code_mapping, - data_extraction_schema=task_v2.extracted_information_schema, - publish_workflow=run_request.publish_workflow, - ), - ) - LOG.error("Invalid agent engine", engine=run_request.engine, organization_id=current_org.organization_id) - raise HTTPException(status_code=400, detail=f"Invalid agent engine: {run_request.engine}") - - -@base_router.post( - "/run/workflows", - tags=["Agent"], - openapi_extra={ - "x-fern-sdk-group-name": "agent", - "x-fern-sdk-method-name": "run_workflow", - "x-fern-examples": [ - { - "code-samples": [ - { - "sdk": "python", - "code": RUN_WORKFLOW_CODE_SAMPLE, - } - ] - } - ], - }, - description="Run a workflow", - summary="Run a workflow", - responses={ - 200: {"description": "Successfully run workflow"}, - 400: {"description": "Invalid workflow run request"}, - }, -) -@base_router.post("/run/workflows/", include_in_schema=False) -async def run_workflow( - request: Request, - background_tasks: BackgroundTasks, - workflow_run_request: WorkflowRunRequest, - current_org: Organization = Depends(org_auth_service.get_current_org), - template: bool = Query(False), - x_api_key: Annotated[str | None, Header()] = None, - x_max_steps_override: Annotated[int | None, Header()] = None, - x_user_agent: Annotated[str | None, Header()] = None, -) -> WorkflowRunResponse: - analytics.capture("skyvern-oss-run-workflow") - await PermissionCheckerFactory.get_instance().check( - current_org, browser_session_id=workflow_run_request.browser_session_id - ) - workflow_id = workflow_run_request.workflow_id - context = skyvern_context.ensure_context() - request_id = context.request_id - legacy_workflow_request = WorkflowRequestBody( - data=workflow_run_request.parameters, - proxy_location=workflow_run_request.proxy_location, - webhook_callback_url=workflow_run_request.webhook_url, - totp_identifier=workflow_run_request.totp_identifier, - totp_url=workflow_run_request.totp_url, - browser_session_id=workflow_run_request.browser_session_id, - ) - workflow_run = await workflow_service.run_workflow( - workflow_id=workflow_id, - organization=current_org, - workflow_request=legacy_workflow_request, - template=template, - version=None, - max_steps=x_max_steps_override, - api_key=x_api_key, - request_id=request_id, - request=request, - background_tasks=background_tasks, - ) - - return WorkflowRunResponse( - run_id=workflow_run.workflow_run_id, - run_type=RunType.workflow_run, - status=str(workflow_run.status), - output=None, - failure_reason=workflow_run.failure_reason, - created_at=workflow_run.created_at, - modified_at=workflow_run.modified_at, - run_request=workflow_run_request, - downloaded_files=None, - recording_url=None, - app_url=f"{settings.SKYVERN_APP_URL.rstrip('/')}/workflows/{workflow_run.workflow_permanent_id}/{workflow_run.workflow_run_id}", - ) - - -@base_router.post( - "/runs/{run_id}/cancel", - tags=["Agent"], - openapi_extra={ - "x-fern-sdk-group-name": "agent", - "x-fern-sdk-method-name": "cancel_run", - }, - description="Cancel a task or workflow run", - summary="Cancel a task or workflow run", -) -@base_router.post("/runs/{run_id}/cancel/", include_in_schema=False) -async def cancel_run( - run_id: str = Path(..., description="The id of the task run or the workflow run to cancel."), - current_org: Organization = Depends(org_auth_service.get_current_org), - x_api_key: Annotated[str | None, Header()] = None, -) -> None: - analytics.capture("skyvern-oss-agent-cancel-run") - - await run_service.cancel_run(run_id, organization_id=current_org.organization_id, api_key=x_api_key) diff --git a/skyvern/forge/sdk/routes/browser_sessions.py b/skyvern/forge/sdk/routes/browser_sessions.py index bc8d8660..11730a8a 100644 --- a/skyvern/forge/sdk/routes/browser_sessions.py +++ b/skyvern/forge/sdk/routes/browser_sessions.py @@ -10,6 +10,58 @@ from skyvern.schemas.browser_sessions import CreateBrowserSessionRequest from skyvern.webeye.schemas import BrowserSessionResponse +@base_router.post( + "/browser_sessions", + response_model=BrowserSessionResponse, + tags=["Browser Sessions"], + openapi_extra={ + "x-fern-sdk-group-name": "browser_session", + "x-fern-sdk-method-name": "create_browser_session", + }, + description="Create a new browser session", + summary="Create a new browser session", + responses={ + 200: {"description": "Successfully created browser session"}, + 403: {"description": "Unauthorized - Invalid or missing authentication"}, + }, +) +async def create_browser_session( + browser_session_request: CreateBrowserSessionRequest, + current_org: Organization = Depends(org_auth_service.get_current_org), +) -> BrowserSessionResponse: + browser_session = await app.PERSISTENT_SESSIONS_MANAGER.create_session( + organization_id=current_org.organization_id, + timeout_minutes=browser_session_request.timeout, + ) + return BrowserSessionResponse.from_browser_session(browser_session) + + +@base_router.post( + "/browser_sessions/{browser_session_id}/close", + tags=["Browser Sessions"], + openapi_extra={ + "x-fern-sdk-group-name": "browser_session", + "x-fern-sdk-method-name": "close_browser_session", + }, + description="Close a browser session", + summary="Close a browser session", + responses={ + 200: {"description": "Successfully closed browser session"}, + 403: {"description": "Unauthorized - Invalid or missing authentication"}, + }, +) +async def close_browser_session( + browser_session_id: str, + current_org: Organization = Depends(org_auth_service.get_current_org), +) -> ORJSONResponse: + await app.PERSISTENT_SESSIONS_MANAGER.close_session(current_org.organization_id, browser_session_id) + return ORJSONResponse( + content={"message": "Browser session closed"}, + status_code=200, + media_type="application/json", + ) + + @base_router.get( "/browser_sessions/{browser_session_id}", response_model=BrowserSessionResponse, @@ -62,55 +114,3 @@ async def get_browser_sessions( analytics.capture("skyvern-oss-agent-browser-sessions-get") browser_sessions = await app.PERSISTENT_SESSIONS_MANAGER.get_active_sessions(current_org.organization_id) return [BrowserSessionResponse.from_browser_session(browser_session) for browser_session in browser_sessions] - - -@base_router.post( - "/browser_sessions", - response_model=BrowserSessionResponse, - tags=["Browser Sessions"], - openapi_extra={ - "x-fern-sdk-group-name": "browser_session", - "x-fern-sdk-method-name": "create_browser_session", - }, - description="Create a new browser session", - summary="Create a new browser session", - responses={ - 200: {"description": "Successfully created browser session"}, - 403: {"description": "Unauthorized - Invalid or missing authentication"}, - }, -) -async def create_browser_session( - browser_session_request: CreateBrowserSessionRequest, - current_org: Organization = Depends(org_auth_service.get_current_org), -) -> BrowserSessionResponse: - browser_session = await app.PERSISTENT_SESSIONS_MANAGER.create_session( - organization_id=current_org.organization_id, - timeout_minutes=browser_session_request.timeout, - ) - return BrowserSessionResponse.from_browser_session(browser_session) - - -@base_router.post( - "/browser_sessions/{browser_session_id}/close", - tags=["Browser Sessions"], - openapi_extra={ - "x-fern-sdk-group-name": "browser_session", - "x-fern-sdk-method-name": "close_browser_session", - }, - description="Close a browser session", - summary="Close a browser session", - responses={ - 200: {"description": "Successfully closed browser session"}, - 403: {"description": "Unauthorized - Invalid or missing authentication"}, - }, -) -async def close_browser_session( - browser_session_id: str, - current_org: Organization = Depends(org_auth_service.get_current_org), -) -> ORJSONResponse: - await app.PERSISTENT_SESSIONS_MANAGER.close_session(current_org.organization_id, browser_session_id) - return ORJSONResponse( - content={"message": "Browser session closed"}, - status_code=200, - media_type="application/json", - ) diff --git a/skyvern/forge/sdk/routes/credentials.py b/skyvern/forge/sdk/routes/credentials.py index d1bb0927..01818ff1 100644 --- a/skyvern/forge/sdk/routes/credentials.py +++ b/skyvern/forge/sdk/routes/credentials.py @@ -19,6 +19,12 @@ from skyvern.forge.sdk.services.bitwarden import BitwardenService LOG = structlog.get_logger() +async def parse_totp_code(content: str) -> str | None: + prompt = prompt_engine.load_prompt("parse-verification-code", content=content) + code_resp = await app.SECONDARY_LLM_API_HANDLER(prompt=prompt, prompt_name="parse-verification-code") + return code_resp.get("code", None) + + @legacy_base_router.post( "/totp", tags=["agent"], @@ -65,184 +71,6 @@ async def send_totp_code( ) -async def parse_totp_code(content: str) -> str | None: - prompt = prompt_engine.load_prompt("parse-verification-code", content=content) - code_resp = await app.SECONDARY_LLM_API_HANDLER(prompt=prompt, prompt_name="parse-verification-code") - return code_resp.get("code", None) - - -@legacy_base_router.get("/credentials") -@legacy_base_router.get("/credentials/", include_in_schema=False) -@base_router.get( - "/credentials", - response_model=list[CredentialResponse], - summary="Get all credentials", - description="Retrieves a paginated list of credentials for the current organization", - tags=["Credentials"], - openapi_extra={ - "x-fern-sdk-group-name": "credentials", - "x-fern-sdk-method-name": "get_credentials", - }, -) -async def get_credentials( - current_org: Organization = Depends(org_auth_service.get_current_org), - page: int = Query( - 1, - ge=1, - description="Page number for pagination", - example=1, - openapi_extra={"x-fern-sdk-parameter-name": "page"}, - ), - page_size: int = Query( - 10, - ge=1, - description="Number of items per page", - example=10, - openapi_extra={"x-fern-sdk-parameter-name": "page_size"}, - ), -) -> list[CredentialResponse]: - organization_bitwarden_collection = await app.DATABASE.get_organization_bitwarden_collection( - current_org.organization_id - ) - if not organization_bitwarden_collection: - return [] - - credentials = await app.DATABASE.get_credentials(current_org.organization_id, page=page, page_size=page_size) - items = await BitwardenService.get_collection_items(organization_bitwarden_collection.collection_id) - - response_items = [] - for credential in credentials: - item = next((item for item in items if item.item_id == credential.item_id), None) - if not item: - continue - if item.credential_type == CredentialType.PASSWORD: - credential_response = PasswordCredentialResponse(username=item.credential.username) - response_items.append( - CredentialResponse( - credential=credential_response, - credential_id=credential.credential_id, - credential_type=item.credential_type, - name=item.name, - ) - ) - elif item.credential_type == CredentialType.CREDIT_CARD: - credential_response = CreditCardCredentialResponse( - last_four=item.credential.card_number[-4:], - brand=item.credential.card_brand, - ) - response_items.append( - CredentialResponse( - credential=credential_response, - credential_id=credential.credential_id, - credential_type=item.credential_type, - name=item.name, - ) - ) - return response_items - - -@legacy_base_router.get("/credentials/{credential_id}") -@legacy_base_router.get("/credentials/{credential_id}/", include_in_schema=False) -@base_router.get( - "/credentials/{credential_id}", - response_model=CredentialResponse, - summary="Get credential by ID", - description="Retrieves a specific credential by its ID", - tags=["Credentials"], - openapi_extra={ - "x-fern-sdk-group-name": "credentials", - "x-fern-sdk-method-name": "get_credential", - }, -) -async def get_credential( - credential_id: str = Path( - ..., - description="The unique identifier of the credential", - example="cred_1234567890", - openapi_extra={"x-fern-sdk-parameter-name": "credential_id"}, - ), - current_org: Organization = Depends(org_auth_service.get_current_org), -) -> CredentialResponse: - organization_bitwarden_collection = await app.DATABASE.get_organization_bitwarden_collection( - current_org.organization_id - ) - if not organization_bitwarden_collection: - raise HTTPException(status_code=404, detail="Credential account not found. It might have been deleted.") - - credential = await app.DATABASE.get_credential( - credential_id=credential_id, organization_id=current_org.organization_id - ) - if not credential: - raise HTTPException(status_code=404, detail="Credential not found") - - credential_item = await BitwardenService.get_credential_item(credential.item_id) - if not credential_item: - raise HTTPException(status_code=404, detail="Credential not found") - - if credential_item.credential_type == CredentialType.PASSWORD: - credential_response = PasswordCredentialResponse( - username=credential_item.credential.username, - ) - return CredentialResponse( - credential=credential_response, - credential_id=credential.credential_id, - credential_type=credential_item.credential_type, - name=credential_item.name, - ) - if credential_item.credential_type == CredentialType.CREDIT_CARD: - credential_response = CreditCardCredentialResponse( - last_four=credential_item.credential.card_number[-4:], - brand=credential_item.credential.card_brand, - ) - return CredentialResponse( - credential=credential_response, - credential_id=credential.credential_id, - credential_type=credential_item.credential_type, - name=credential_item.name, - ) - raise HTTPException(status_code=400, detail="Invalid credential type") - - -@legacy_base_router.delete("/credentials/{credential_id}") -@legacy_base_router.delete("/credentials/{credential_id}/", include_in_schema=False) -@base_router.post( - "/credentials/{credential_id}/delete", - status_code=204, - summary="Delete credential", - description="Deletes a specific credential by its ID", - tags=["Credentials"], - openapi_extra={ - "x-fern-sdk-group-name": "credentials", - "x-fern-sdk-method-name": "delete_credential", - }, -) -async def delete_credential( - credential_id: str = Path( - ..., - description="The unique identifier of the credential to delete", - example="cred_1234567890", - openapi_extra={"x-fern-sdk-parameter-name": "credential_id"}, - ), - current_org: Organization = Depends(org_auth_service.get_current_org), -) -> None: - organization_bitwarden_collection = await app.DATABASE.get_organization_bitwarden_collection( - current_org.organization_id - ) - if not organization_bitwarden_collection: - raise HTTPException(status_code=404, detail="Credential account not found. It might have been deleted.") - - credential = await app.DATABASE.get_credential( - credential_id=credential_id, organization_id=current_org.organization_id - ) - if not credential: - raise HTTPException(status_code=404, detail=f"Credential not found, credential_id={credential_id}") - - await app.DATABASE.delete_credential(credential.credential_id, current_org.organization_id) - await BitwardenService.delete_credential_item(credential.item_id) - - return None - - @legacy_base_router.post("/credentials") @legacy_base_router.post("/credentials/", include_in_schema=False) @base_router.post( @@ -319,3 +147,175 @@ async def create_credential( credential_type=data.credential_type, name=data.name, ) + + +@legacy_base_router.delete("/credentials/{credential_id}") +@legacy_base_router.delete("/credentials/{credential_id}/", include_in_schema=False) +@base_router.post( + "/credentials/{credential_id}/delete", + status_code=204, + summary="Delete credential", + description="Deletes a specific credential by its ID", + tags=["Credentials"], + openapi_extra={ + "x-fern-sdk-group-name": "credentials", + "x-fern-sdk-method-name": "delete_credential", + }, +) +async def delete_credential( + credential_id: str = Path( + ..., + description="The unique identifier of the credential to delete", + example="cred_1234567890", + openapi_extra={"x-fern-sdk-parameter-name": "credential_id"}, + ), + current_org: Organization = Depends(org_auth_service.get_current_org), +) -> None: + organization_bitwarden_collection = await app.DATABASE.get_organization_bitwarden_collection( + current_org.organization_id + ) + if not organization_bitwarden_collection: + raise HTTPException(status_code=404, detail="Credential account not found. It might have been deleted.") + + credential = await app.DATABASE.get_credential( + credential_id=credential_id, organization_id=current_org.organization_id + ) + if not credential: + raise HTTPException(status_code=404, detail=f"Credential not found, credential_id={credential_id}") + + await app.DATABASE.delete_credential(credential.credential_id, current_org.organization_id) + await BitwardenService.delete_credential_item(credential.item_id) + + return None + + +@legacy_base_router.get("/credentials/{credential_id}") +@legacy_base_router.get("/credentials/{credential_id}/", include_in_schema=False) +@base_router.get( + "/credentials/{credential_id}", + response_model=CredentialResponse, + summary="Get credential by ID", + description="Retrieves a specific credential by its ID", + tags=["Credentials"], + openapi_extra={ + "x-fern-sdk-group-name": "credentials", + "x-fern-sdk-method-name": "get_credential", + }, +) +async def get_credential( + credential_id: str = Path( + ..., + description="The unique identifier of the credential", + example="cred_1234567890", + openapi_extra={"x-fern-sdk-parameter-name": "credential_id"}, + ), + current_org: Organization = Depends(org_auth_service.get_current_org), +) -> CredentialResponse: + organization_bitwarden_collection = await app.DATABASE.get_organization_bitwarden_collection( + current_org.organization_id + ) + if not organization_bitwarden_collection: + raise HTTPException(status_code=404, detail="Credential account not found. It might have been deleted.") + + credential = await app.DATABASE.get_credential( + credential_id=credential_id, organization_id=current_org.organization_id + ) + if not credential: + raise HTTPException(status_code=404, detail="Credential not found") + + credential_item = await BitwardenService.get_credential_item(credential.item_id) + if not credential_item: + raise HTTPException(status_code=404, detail="Credential not found") + + if credential_item.credential_type == CredentialType.PASSWORD: + credential_response = PasswordCredentialResponse( + username=credential_item.credential.username, + ) + return CredentialResponse( + credential=credential_response, + credential_id=credential.credential_id, + credential_type=credential_item.credential_type, + name=credential_item.name, + ) + if credential_item.credential_type == CredentialType.CREDIT_CARD: + credential_response = CreditCardCredentialResponse( + last_four=credential_item.credential.card_number[-4:], + brand=credential_item.credential.card_brand, + ) + return CredentialResponse( + credential=credential_response, + credential_id=credential.credential_id, + credential_type=credential_item.credential_type, + name=credential_item.name, + ) + raise HTTPException(status_code=400, detail="Invalid credential type") + + +@legacy_base_router.get("/credentials") +@legacy_base_router.get("/credentials/", include_in_schema=False) +@base_router.get( + "/credentials", + response_model=list[CredentialResponse], + summary="Get all credentials", + description="Retrieves a paginated list of credentials for the current organization", + tags=["Credentials"], + openapi_extra={ + "x-fern-sdk-group-name": "credentials", + "x-fern-sdk-method-name": "get_credentials", + }, +) +async def get_credentials( + current_org: Organization = Depends(org_auth_service.get_current_org), + page: int = Query( + 1, + ge=1, + description="Page number for pagination", + example=1, + openapi_extra={"x-fern-sdk-parameter-name": "page"}, + ), + page_size: int = Query( + 10, + ge=1, + description="Number of items per page", + example=10, + openapi_extra={"x-fern-sdk-parameter-name": "page_size"}, + ), +) -> list[CredentialResponse]: + organization_bitwarden_collection = await app.DATABASE.get_organization_bitwarden_collection( + current_org.organization_id + ) + if not organization_bitwarden_collection: + return [] + + credentials = await app.DATABASE.get_credentials(current_org.organization_id, page=page, page_size=page_size) + items = await BitwardenService.get_collection_items(organization_bitwarden_collection.collection_id) + + response_items = [] + for credential in credentials: + item = next((item for item in items if item.item_id == credential.item_id), None) + if not item: + continue + if item.credential_type == CredentialType.PASSWORD: + credential_response = PasswordCredentialResponse(username=item.credential.username) + response_items.append( + CredentialResponse( + credential=credential_response, + credential_id=credential.credential_id, + credential_type=item.credential_type, + name=item.name, + ) + ) + elif item.credential_type == CredentialType.CREDIT_CARD: + credential_response = CreditCardCredentialResponse( + last_four=item.credential.card_number[-4:], + brand=item.credential.card_brand, + ) + response_items.append( + CredentialResponse( + credential=credential_response, + credential_id=credential.credential_id, + credential_type=item.credential_type, + name=item.name, + ) + ) + return response_items