From bd2f202f854a9b8658c1ee6e03c1421a160903c5 Mon Sep 17 00:00:00 2001 From: Kim Burgaard Date: Thu, 4 Jun 2026 21:48:20 -0700 Subject: [PATCH 1/3] Updated the SDK to handle attachments #minor --- README.md | 23 + openapi/seclai.openapi.json | 652 ++++++++++++++++-- .../api/agent_run_attachments/__init__.py | 1 + ...ns_run_id_attachments_attachment_id_get.py | 281 ++++++++ ...ents_agent_id_attachment_references_get.py | 208 ++++++ ...t_api_agents_agent_id_upload_input_post.py | 24 + ...tion_api_agents_agent_id_definition_get.py | 48 ++ ...run_agent_api_agents_agent_id_runs_post.py | 26 +- ...nt_api_agents_agent_id_runs_stream_post.py | 26 +- ...tion_api_agents_agent_id_definition_put.py | 52 +- ..._connection_content_version_upload_post.py | 4 + ...ground_experiments_experiment_id_delete.py | 210 ++++++ ...ources_source_connection_id_upload_post.py | 4 + seclai/_generated/models/__init__.py | 12 + .../agent_attachment_refs_api_response.py | 97 +++ .../agent_definition_import_error_response.py | 27 +- .../models/agent_definition_response.py | 2 +- .../agent_definition_response_definition.py | 2 +- seclai/_generated/models/agent_run_request.py | 92 ++- .../_generated/models/agent_run_response.py | 48 ++ .../models/agent_run_step_response.py | 33 +- .../models/agent_run_stream_request.py | 78 ++- .../models/agent_run_tool_call_response.py | 228 ++++++ .../models/agent_summary_response.py | 34 +- .../models/ai_assistant_generate_request.py | 38 +- .../attachment_refs_source_api_summary.py | 106 +++ .../_generated/models/import_skip_response.py | 4 +- .../models/insufficient_credits_detail.py | 82 +++ .../models/insufficient_credits_response.py | 68 ++ .../memory_bank_ai_assistant_request.py | 54 +- .../models/modality_rate_response.py | 116 ++++ ...ding_processing_completed_failed_status.py | 1 + .../models/prompt_model_response.py | 58 ++ seclai/seclai.py | 168 +++++ tests/test_new_methods.py | 99 +++ 35 files changed, 2874 insertions(+), 132 deletions(-) create mode 100644 seclai/_generated/api/agent_run_attachments/__init__.py create mode 100644 seclai/_generated/api/agent_run_attachments/serve_agent_run_attachment_api_v2_agent_runs_run_id_attachments_attachment_id_get.py create mode 100644 seclai/_generated/api/agents/api_get_agent_attachment_references_api_agents_agent_id_attachment_references_get.py create mode 100644 seclai/_generated/api/models/delete_experiment_endpoint_api_models_playground_experiments_experiment_id_delete.py create mode 100644 seclai/_generated/models/agent_attachment_refs_api_response.py create mode 100644 seclai/_generated/models/agent_run_tool_call_response.py create mode 100644 seclai/_generated/models/attachment_refs_source_api_summary.py create mode 100644 seclai/_generated/models/insufficient_credits_detail.py create mode 100644 seclai/_generated/models/insufficient_credits_response.py create mode 100644 seclai/_generated/models/modality_rate_response.py diff --git a/README.md b/README.md index 6b0b9d2..d2ce298 100644 --- a/README.md +++ b/README.md @@ -260,10 +260,26 @@ result = client.run_agent_and_poll( ### Agent input uploads ```python +# Discover which files (if any) the agent expects before staging uploads +refs = client.get_agent_attachment_references("agent_id") +# refs["requires_uploads"] -> bool; refs["agent"] lists the exact_names / +# indexes_max / patterns a run-time upload batch must satisfy. + upload = client.upload_agent_input("agent_id", file=b"data", file_name="input.pdf") status = client.get_agent_input_upload_status("agent_id", upload["upload_id"]) ``` +### Agent run attachments + +```python +# Download a file emitted by a step in an agent run. attachment_id is the +# URL-safe-base64 storage_key surfaced in run output manifests / webhooks. +response = client.download_agent_run_attachment("run_id", "attachment_id") # raw httpx.Response +with response: + for chunk in response.iter_bytes(): + ... # write to disk +``` + ### Agent AI assistant ```python @@ -491,6 +507,13 @@ client.mark_model_alert_read("alert_id") client.mark_all_model_alerts_read() unread = client.get_unread_model_alert_count() recs = client.get_model_recommendations("model_id") + +# Model playground experiments +experiment = client.create_experiment({"model_ids": ["model_id"], "prompt": "..."}) +experiments = client.list_experiments() +detail = client.get_experiment("experiment_id") +client.cancel_experiment("experiment_id") +client.delete_experiment("experiment_id") # soft-delete, preserves audit history ``` ### Search diff --git a/openapi/seclai.openapi.json b/openapi/seclai.openapi.json index 16b3434..53fa909 100644 --- a/openapi/seclai.openapi.json +++ b/openapi/seclai.openapi.json @@ -52,11 +52,31 @@ "title": "AddConversationTurnRequest", "type": "object" }, + "AgentAttachmentRefsApiResponse": { + "description": "Static attachment-reference contract for an agent.\n\nMirrors the MCP ``get_agent_attachment_references`` tool: returns\nwhat files (if any) an agent's templates expect on a run so API\nconsumers can stage uploads correctly before calling\n``POST /agents/{id}/runs``.", + "properties": { + "agent": { + "$ref": "#/components/schemas/AttachmentRefsSourceApiSummary", + "description": "Aggregated selector summary across all consumer steps. ``exact_names`` entries must each appear in the upload batch; ``indexes_max+1`` is the minimum file count; every ``patterns`` glob must match at least one upload." + }, + "requires_uploads": { + "description": "When ``false`` the agent's definition does NOT reference any uploaded attachments \u2014 ``POST /agents/{id}/upload-input`` will reject with HTTP 400. When ``true`` the ``agent`` block lists the specific selectors a run-time batch must satisfy.", + "title": "Requires Uploads", + "type": "boolean" + } + }, + "required": [ + "requires_uploads" + ], + "title": "AgentAttachmentRefsApiResponse", + "type": "object" + }, "AgentDefinitionImportErrorResponse": { - "description": "422 body for invalid `agent_definition` payloads.\n\nMirrors :py:meth:`AgentDefinitionImportError.to_response_dict`.", + "description": "422 body for invalid `agent_definition` payloads.\n\nMirrors `AgentDefinitionImportError.to_response_dict`.", "properties": { "error": { - "default": "invalid_agent_definition", + "const": "invalid_agent_definition", + "description": "Stable machine-readable error code.", "title": "Error", "type": "string" }, @@ -78,6 +98,7 @@ } }, "required": [ + "error", "message", "errors", "source" @@ -94,7 +115,7 @@ }, "definition": { "additionalProperties": true, - "description": "The agent definition containing name, description, tags, and step workflow tree. Step types include prompt_call, retrieval, regex_replace, gate, retry, evaluate_step, extract_data, extract_content, add_chat_turn, load_chat_history, add_memory, search_memory, load_memory, streaming_result, send_email, webhook_call, call_agent, write_metadata, write_content_attachment, load_content_attachment, load_content, display_result, merge, for_each, and others.", + "description": "The agent definition containing name, description, tags, and step workflow tree. Step types include prompt_call, retrieval, regex_replace, gate, retry, evaluate_step, extract_data, extract_content, add_chat_turn, load_chat_history, add_memory, search_memory, load_memory, streaming_result, send_email, webhook_call, call_agent, write_metadata, write_content_attachment, load_content_attachment, load_content, display_result, merge, for_each, if_else, switch, and others.", "title": "Definition", "type": "object" }, @@ -337,9 +358,26 @@ "type": "null" } ], - "description": "ID of a previously uploaded file (via POST /{agent_id}/upload-input) to use as the run input for dynamic-input triggers. Mutually exclusive with the 'input' field.", + "description": "ID of a previously uploaded file (via POST /{agent_id}/upload-input) to use as the run input for dynamic-input triggers. Mutually exclusive with the 'input' field. Use ``input_upload_ids`` to attach multiple files.\n\n**Attachment visibility:** a step only sees the upload when its template references the input \u2014 via ``{{input}}`` / ``{{agent.input}}`` / ``{{step..input|output}}`` (implicit, all attachments) or the ``{{attachments[\u2026]}}`` family (explicit narrowing \u2014 e.g. ``{{attachments[0]}}``, ``{{attachments[*.pdf]}}``).\n\n**Per-batch validation:** every selector the agent's definition declares must be satisfied or the run is rejected with HTTP 400. Exact-name selectors require that filename to be present; indexed selectors require at least N+1 files; glob patterns require at least one matching filename.", "title": "Input Upload Id" }, + "input_upload_ids": { + "anyOf": [ + { + "items": { + "format": "uuid", + "type": "string" + }, + "maxItems": 20, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "IDs of multiple previously uploaded files. Each upload's extracted text is concatenated under a heading; each upload's binary is surfaced as a separate ``MediaAttachment`` so multi-modal prompt steps reason over all files at once. Steps narrow visibility via ``{{attachments[\u2026]}}`` selectors (by index, filename, or fnmatch glob). The batch must satisfy every selector the agent declares \u2014 exact names, indexed references (length must exceed the highest index), and glob patterns (each pattern needs at least one match). Mismatches return HTTP 400 with the unmet requirements listed. Mutually exclusive with ``input`` and ``input_upload_id`` \u2014 pass exactly one of the three. Max 20 uploads per run.", + "title": "Input Upload Ids" + }, "metadata": { "anyOf": [ { @@ -360,6 +398,19 @@ "description": "If true, the agent run will be treated as priority execution.", "title": "Priority", "type": "boolean" + }, + "replay_of_run_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Re-run this agent reusing a prior run's uploaded input files. The files are re-resolved server-side from the source run (which must belong to this account and agent) \u2014 you do not re-upload them. Combine with ``input`` to change the text while keeping the files. A fresh upload batch (``input_upload_id(s)``) takes precedence and disables replay. Binaries swept by retention fall back to their extracted text.", + "title": "Replay Of Run Id" } }, "title": "AgentRunRequest", @@ -432,6 +483,18 @@ "description": "Milliseconds spent waiting for governance input evaluation.", "title": "Governance Input Wait Ms" }, + "hitl_wait_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Cumulative milliseconds the run was parked waiting for a human decision on a human_in_the_loop step. Subtracted from active duration in run-detail and duration-stats responses.", + "title": "Hitl Wait Ms" + }, "input": { "anyOf": [ { @@ -468,6 +531,18 @@ "description": "Output produced by the agent run.", "title": "Output" }, + "output_content_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "MIME type of `output` \u2014 mirrors the terminal step's `output_content_type`. Consumers interpret `output` differently depending on this value: `application/vnd.seclai.manifest+json` is a multi-asset manifest with shape `{text, attachments: [{storage_key, mime, name, bytes}]}` \u2014 fetch each attachment via `GET /authenticated/storage-blobs/{storage_key}`. `text/plain` / `text/*` are free-form text. `application/json` is a JSON document. Null on runs that produced no terminal output or that pre-date this column.", + "title": "Output Content Type" + }, "priority": { "description": "Indicates if the run was treated as a priority execution.", "title": "Priority", @@ -615,6 +690,14 @@ "description": "Type of the agent step.", "title": "Step Type", "type": "string" + }, + "tool_calls": { + "description": "LLM tool calls made during this step (prompt_call steps only), ordered by execution. Empty for steps that invoked no tools.", + "items": { + "$ref": "#/components/schemas/AgentRunToolCallResponse" + }, + "title": "Tool Calls", + "type": "array" } }, "required": [ @@ -656,9 +739,26 @@ "type": "null" } ], - "description": "ID of a previously uploaded file (via POST /{agent_id}/upload-input) to use as the run input for dynamic-input triggers. Mutually exclusive with the 'input' field.", + "description": "ID of a previously uploaded file (via POST /{agent_id}/upload-input) to use as the run input for dynamic-input triggers. Mutually exclusive with the 'input' field. Use ``input_upload_ids`` to attach multiple files. Subject to the same per-batch attachment-selector validation as the non-streaming endpoint.", "title": "Input Upload Id" }, + "input_upload_ids": { + "anyOf": [ + { + "items": { + "format": "uuid", + "type": "string" + }, + "maxItems": 20, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "IDs of multiple previously uploaded files. See the non-streaming endpoint for full semantics, including per-batch selector validation (exact names, indexed references, and glob patterns must all be satisfied or the run is rejected with HTTP 400). Max 20.", + "title": "Input Upload Ids" + }, "metadata": { "anyOf": [ { @@ -673,11 +773,141 @@ ], "description": "Metadata to make available for string substitution expressions in agent tasks.", "title": "Metadata" + }, + "replay_of_run_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Re-run reusing a prior run's uploaded input files (re-resolved server-side from the source run, which must belong to this account and agent). A fresh upload batch takes precedence.", + "title": "Replay Of Run Id" } }, "title": "AgentRunStreamRequest", "type": "object" }, + "AgentRunToolCallResponse": { + "description": "A single LLM tool call made during a prompt_call step.", + "properties": { + "credits_used": { + "default": 0.0, + "description": "Credits consumed by this tool call (0 for tools that don't bill).", + "title": "Credits Used", + "type": "number" + }, + "duration_seconds": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "description": "Duration of the tool call in seconds.", + "title": "Duration Seconds" + }, + "ended_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Timestamp when the tool call ended.", + "title": "Ended At" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Error message when the tool call failed.", + "title": "Error" + }, + "function_name": { + "description": "Name of the tool/function invoked.", + "title": "Function Name", + "type": "string" + }, + "id": { + "description": "Tool call identifier.", + "title": "Id", + "type": "string" + }, + "input": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "JSON arguments the LLM passed to the tool, if persisted.", + "title": "Input" + }, + "output": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "JSON result the tool returned to the LLM, if persisted.", + "title": "Output" + }, + "round_index": { + "default": 0, + "description": "0-based tool-loop round this call belonged to.", + "title": "Round Index", + "type": "integer" + }, + "sequence": { + "default": 0, + "description": "0-based ordinal of this call within its step run.", + "title": "Sequence", + "type": "integer" + }, + "started_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Timestamp when the tool call started.", + "title": "Started At" + }, + "succeeded": { + "default": true, + "description": "Whether the tool call completed without error.", + "title": "Succeeded", + "type": "boolean" + } + }, + "required": [ + "id", + "function_name" + ], + "title": "AgentRunToolCallResponse", + "type": "object" + }, "AgentSummaryResponse": { "properties": { "created_at": { @@ -814,7 +1044,6 @@ "id", "name", "description", - "trigger_type", "created_at", "updated_at" ], @@ -1042,21 +1271,6 @@ "title": "AiAssistantFeedbackResponse", "type": "object" }, - "AiAssistantGenerateRequest": { - "description": "Request body for AI assistant generate endpoints.", - "properties": { - "user_input": { - "description": "User input describing what to do", - "title": "User Input", - "type": "string" - } - }, - "required": [ - "user_input" - ], - "title": "AiAssistantGenerateRequest", - "type": "object" - }, "AiAssistantGenerateResponse": { "description": "Response from an AI assistant generate endpoint.", "properties": { @@ -1268,6 +1482,45 @@ "title": "AppliedActionResponse", "type": "object" }, + "AttachmentRefsSourceApiSummary": { + "description": "Per-source attachment-reference summary.", + "properties": { + "exact_names": { + "items": { + "type": "string" + }, + "title": "Exact Names", + "type": "array" + }, + "indexes_max": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Indexes Max" + }, + "kinds": { + "items": { + "type": "string" + }, + "title": "Kinds", + "type": "array" + }, + "patterns": { + "items": { + "type": "string" + }, + "title": "Patterns", + "type": "array" + } + }, + "title": "AttachmentRefsSourceApiSummary", + "type": "object" + }, "Body_upload_file_to_content_api_contents__source_connection_content_version__upload_post": { "properties": { "file": { @@ -3291,7 +3544,7 @@ "type": "object" }, "ImportSkipResponse": { - "description": "One item that was not applied during an agent import.\n\nUsed as the element type for ``import_warnings`` on every\nresponse model that accepts an ``agent_definition`` payload.\nSee :py:class:`services.agent_definition_import.AgentImportSkip`\nfor the full category list.\n\nLives here (not on each router) so the authenticated and public\nAPI responses share one definition \u2014 keeping the shape that\nclients (UI modal, MCP, OpenAPI consumers) depend on aligned.", + "description": "One item that was not applied during an agent import.\n\nUsed as the element type for ``import_warnings`` on every\nresponse model that accepts an ``agent_definition`` payload.\nSee ``services.agent_definition_import.AgentImportSkip`` for the\nfull category list.\n\nLives here (not on each router) so the authenticated and public\nAPI responses share one definition \u2014 keeping the shape that\nclients (UI modal, MCP, OpenAPI consumers) depend on aligned.", "properties": { "category": { "description": "The kind of item that was skipped or substituted: 'schedule', 'evaluation_criteria', 'alert_config', 'alert_recipient', 'governance_policy', 'governance_kb_link', 'solution_link'.", @@ -3423,6 +3676,47 @@ "title": "InlineTextUploadRequest", "type": "object" }, + "InsufficientCreditsDetail": { + "description": "``detail`` body for a 402 ``insufficient_credits`` response.", + "properties": { + "account_id": { + "description": "UUID of the account that ran out of credits.", + "title": "Account Id", + "type": "string" + }, + "error": { + "const": "insufficient_credits", + "description": "Stable machine-readable error code.", + "title": "Error", + "type": "string" + }, + "message": { + "description": "Human-readable explanation.", + "title": "Message", + "type": "string" + } + }, + "required": [ + "error", + "message", + "account_id" + ], + "title": "InsufficientCreditsDetail", + "type": "object" + }, + "InsufficientCreditsResponse": { + "description": "402 envelope returned when the account has exhausted its credits.", + "properties": { + "detail": { + "$ref": "#/components/schemas/InsufficientCreditsDetail" + } + }, + "required": [ + "detail" + ], + "title": "InsufficientCreditsResponse", + "type": "object" + }, "JsonValue": {}, "KnowledgeBaseListResponseModel": { "description": "Paginated list of knowledge bases.", @@ -4006,6 +4300,42 @@ "title": "MemoryBank", "type": "object" }, + "ModalityRateResponse": { + "description": "Per-modality rate for an LLM that prices image/audio/video output\n(or input) at a rate distinct from the default text rate.\n\nExample: Gemini 3.1 Flash Image charges $3/1M output tokens for\ntext but $60/1M output tokens for generated images. The image rate\nsurfaces here with ``modality=\"image\"`` and ``output_credits_per_1000_tokens``\nset; the default text rate stays on the parent model fields.", + "properties": { + "input_credits_per_1000_tokens": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Input Credits Per 1000 Tokens" + }, + "modality": { + "title": "Modality", + "type": "string" + }, + "output_credits_per_1000_tokens": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Output Credits Per 1000 Tokens" + } + }, + "required": [ + "modality" + ], + "title": "ModalityRateResponse", + "type": "object" + }, "OrganizationAlertPreferenceListResponse": { "properties": { "preferences": { @@ -4096,7 +4426,8 @@ "pending", "processing", "completed", - "failed" + "failed", + "waiting_human" ], "title": "PendingProcessingCompletedFailedStatus", "type": "string" @@ -7051,30 +7382,31 @@ "routers__api__memory_banks__MemoryBankAiAssistantRequest": { "description": "Request body for the memory bank AI assistant.", "properties": { - "conversation_id": { + "current_config": { "anyOf": [ { - "type": "string" + "additionalProperties": true, + "type": "object" }, { "type": "null" } ], - "description": "Previous conversation ID to continue.", - "title": "Conversation Id" + "description": "Current configuration to refine, if any.", + "title": "Current Config" }, - "current_config": { + "history_since": { "anyOf": [ { - "additionalProperties": true, - "type": "object" + "format": "date-time", + "type": "string" }, { "type": "null" } ], - "description": "Current configuration to refine, if any.", - "title": "Current Config" + "description": "Optional ISO 8601 timestamp. When set, only conversation turns created at or after this timestamp are loaded as context, scoping history to the current session so the assistant remembers earlier turns in a multi-turn refinement.", + "title": "History Since" }, "user_input": { "description": "Natural-language description of the memory bank.", @@ -7240,6 +7572,34 @@ "title": "AiAssistantAcceptRequest", "type": "object" }, + "routers__api__solutions__AiAssistantGenerateRequest": { + "description": "Request body for AI assistant generate endpoints.", + "properties": { + "history_since": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional ISO 8601 timestamp. When set, only conversation turns created at or after this timestamp are loaded as context, scoping history to the current session so the assistant remembers earlier turns in a create flow.", + "title": "History Since" + }, + "user_input": { + "description": "User input describing what to do", + "title": "User Input", + "type": "string" + } + }, + "required": [ + "user_input" + ], + "title": "AiAssistantGenerateRequest", + "type": "object" + }, "routers__api__solutions__SolutionAgentResponse": { "properties": { "id": { @@ -7868,6 +8228,18 @@ "description": "ID of the created content version", "title": "Content Version Id" }, + "embedder_warning": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Set when the file is non-text but the source's embedder is text-only \u2014 indexing will rely on OCR / transcription and may produce a FAILED row if no text can be extracted.", + "title": "Embedder Warning" + }, "filename": { "description": "Original filename", "title": "Filename", @@ -8084,6 +8456,13 @@ "description": "Source URL used to derive payload_schema guidance for this model.", "title": "Payload Schema Source Url" }, + "per_modality_rates": { + "items": { + "$ref": "#/components/schemas/ModalityRateResponse" + }, + "title": "Per Modality Rates", + "type": "array" + }, "provider": { "title": "Provider", "type": "string" @@ -8175,6 +8554,20 @@ ], "title": "Supported Languages" }, + "supported_output_media": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Supported Output Media" + }, "supports_openai_arguments": { "default": false, "title": "Supports Openai Arguments", @@ -9662,9 +10055,55 @@ ] } }, + "/agents/{agent_id}/attachment-references": { + "get": { + "description": "Return the static attachment-reference contract for an agent \u2014 what files the agent's definition expects on a run.\n\nCall this BEFORE staging uploads so you know whether the agent accepts files at all (``requires_uploads``), and which specific filenames/indexes/patterns the templates reference. Mismatched batches are rejected at run time with HTTP 400.", + "operationId": "api_get_agent_attachment_references_api_agents__agent_id__attachment_references_get", + "parameters": [ + { + "in": "path", + "name": "agent_id", + "required": true, + "schema": { + "title": "Agent Id", + "type": "string" + } + }, + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentAttachmentRefsApiResponse" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Get agent attachment-reference contract", + "tags": [ + "agents" + ] + } + }, "/agents/{agent_id}/definition": { "get": { - "description": "Fetch the current agent definition from the main branch.\n\nThe response includes `change_id` which must be provided when updating the definition (optimistic locking).\n\nThe definition contains the agent's step workflow. Available step types:\n- `prompt_call`: Call an LLM with a prompt template\n- `retrieval`: Search a knowledge base\n- `regex_replace`: Reshape text via ordered regex find/replace rules\n- `gate`: Evaluate conditions, stop or continue child execution\n- `retry`: Re-execute from a target ancestor step (for quality-control loops; pair with a `gate` step for conditional retrying. Fields: `target_step_id` (ancestor step ID), `max_retries` (1\u201310))\n- `evaluate_step`: Score a selected previous step output and emit JSON with `score`, `passed`, and `pass_threshold` (fields: `target_step_id`, `evaluation_prompt`, `pass_threshold`, optional `evaluation_tier`, optional `expectation_config`)\n- `extract_data`: Progressively read and analyze large input\n- `extract_content`: Extract structured data (JSON, HTML, XML)\n- `add_chat_turn` / `load_chat_history`: Record a turn or load running history from a conversation memory bank\n- `add_memory` / `search_memory` / `load_memory`: Write, semantic-search, or load entries on a general memory bank\n- `send_email`: Send email with step output\n- `webhook_call`: POST data to an external URL\n- `write_aws_s3_object`: Write output to S3\n- `call_agent`: Invoke another agent\n- `write_metadata`: Write a value to content metadata (for filtering/gates; content-triggered agents only. Fields: `metadata_key`, `content`)\n- `write_content_attachment`: Write a file-backed attachment to content (optionally indexed for retrieval; content-triggered agents only. Fields: `attachment_key`, `content`, `content_type`, `indexed`)\n- `load_content_attachment`: Load a previously written attachment (content-triggered agents only. Fields: `attachment_key`)\n- `load_content`: Load the full text body of a source document (typically used with content-triggered agents; can also load by explicit `content_version_id`. Fields: `content_version_id` optional)\n- `streaming_result`: Stream LLM tokens in real-time via SSE (must be a direct child of `prompt_call`; requires `dynamic_input` or `template_input` trigger; `priority: true` enables real-time streaming)\n- `display_result`: Show output to the user\n- `join`: Merge parallel branches\n- `merge`: Combine multiple inputs into a single templated output\n- `text`: Static text literal\n- `for_each`: Iterate a body over a list of items (body lives in `body[]`)\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. You can only access agents belonging to your account.", + "description": "Fetch the current agent definition from the main branch.\n\nThe response includes `change_id` which must be provided when updating the definition (optimistic locking).\n\nThe definition contains the agent's step workflow. Available step types:\n- `prompt_call`: Call an LLM with a prompt template\n- `retrieval`: Search a knowledge base\n- `regex_replace`: Reshape text via ordered regex find/replace rules\n- `gate`: Evaluate conditions, stop or continue child execution\n- `retry`: Re-execute from a target ancestor step (for quality-control loops; pair with a `gate` step for conditional retrying. Fields: `target_step_id` (ancestor step ID), `max_retries` (1\u201310))\n- `evaluate_step`: Score a selected previous step output and emit JSON with `score`, `passed`, and `pass_threshold` (fields: `target_step_id`, `evaluation_prompt`, `pass_threshold`, optional `evaluation_tier`, optional `expectation_config`)\n- `extract_data`: Progressively read and analyze large input\n- `extract_content`: Extract structured data (JSON, HTML, XML)\n- `add_chat_turn` / `load_chat_history`: Record a turn or load running history from a conversation memory bank\n- `add_memory` / `search_memory` / `load_memory`: Write, semantic-search, or load entries on a general memory bank\n- `send_email`: Send email with step output\n- `webhook_call`: POST data to an external URL\n- `write_aws_s3_object`: Write output to S3\n- `call_agent`: Invoke another agent\n- `write_metadata`: Write a value to content metadata (for filtering/gates; content-triggered agents only. Fields: `metadata_key`, `content`)\n- `write_content_attachment`: Write a file-backed attachment to content (optionally indexed for retrieval; content-triggered agents only. Fields: `attachment_key`, `content`, `content_type`, `indexed`)\n- `load_content_attachment`: Load a previously written attachment (content-triggered agents only. Fields: `attachment_key`)\n- `load_content`: Load the full text body of a source document (typically used with content-triggered agents; can also load by explicit `content_version_id`. Fields: `content_version_id` optional)\n- `streaming_result`: Stream LLM tokens in real-time via SSE (must be a direct child of `prompt_call`; requires `dynamic_input` or `template_input` trigger; `priority: true` enables real-time streaming)\n- `display_result`: Show output to the user\n- `join`: Merge parallel branches\n- `merge`: Combine multiple inputs into a single templated output\n- `text`: Static text literal\n- `for_each`: Iterate a body over a list of items (body lives in `body[]`)\n- `if_else`: Conditional dispatch. Evaluates `conditions` (same shape as `gate`) and runs `then_steps` on match, otherwise the optional `else_steps`. The chosen branch's output flows to the if_else step's own `child_steps` (post-branch continuation chain). **`display_result` and `streaming_result` are not allowed inside `then_steps` / `else_steps`** \u2014 end each branch with a content-producing step (e.g. `text`, `prompt_call`) and place the single `display_result` in `child_steps`.\n- `switch`: Single-discriminator dispatch. Renders `discriminator` (default `{{input}}`) and routes to the first matching `cases[]` entry (equality by default; pass a list in `match` for `$in` semantics) or to `else_steps` when nothing matches. The chosen case's output flows to the switch step's own `child_steps`. **`display_result` and `streaming_result` are not allowed inside `cases[].steps` or `else_steps`** \u2014 end each case with a content-producing step and place the single `display_result` in `child_steps`.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. You can only access agents belonging to your account.", "operationId": "get_agent_definition_api_agents__agent_id__definition_get", "parameters": [ { @@ -9708,7 +10147,7 @@ ] }, "put": { - "description": "Update the agent's definition on the main branch.\n\nUses **optimistic locking**: provide `expected_change_id` from the last `GET /api/agents/{agent_id}/definition`. Returns `409 Conflict` if the definition was modified since your last read.\n\nThe definition contains the agent's step workflow. Step types include `prompt_call`, `retrieval`, `regex_replace`, `gate`, `retry`, `evaluate_step`, `extract_data`, `extract_content`, `add_chat_turn`, `load_chat_history`, `add_memory`, `search_memory`, `load_memory`, `streaming_result`, `send_email`, `webhook_call`, `write_aws_s3_object`, `call_agent`, `write_metadata`, `write_content_attachment`, `load_content_attachment`, `load_content`, `display_result`, `join`, `merge`, `text`, and `for_each`. Non-composite step types (`display_result`, `join`, `retry`, `streaming_result`) cannot contain child steps.\n\n**Retry steps** re-execute from a target ancestor step for quality-control loops. Configure with `target_step_id` (ancestor step ID) and `max_retries` (1\u201310). Best practice: place a `gate` step before the retry to make retries conditional.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. You can only update agents belonging to your account.", + "description": "Update the agent's definition on the main branch.\n\nUses **optimistic locking**: provide `expected_change_id` from the last `GET /api/agents/{agent_id}/definition`. Returns `409 Conflict` if the definition was modified since your last read.\n\nThe definition contains the agent's step workflow. Step types include `prompt_call`, `retrieval`, `regex_replace`, `gate`, `retry`, `evaluate_step`, `extract_data`, `extract_content`, `add_chat_turn`, `load_chat_history`, `add_memory`, `search_memory`, `load_memory`, `streaming_result`, `send_email`, `webhook_call`, `write_aws_s3_object`, `call_agent`, `write_metadata`, `write_content_attachment`, `load_content_attachment`, `load_content`, `display_result`, `join`, `merge`, `text`, `for_each`, `if_else`, and `switch`. Non-composite step types (`display_result`, `join`, `retry`, `streaming_result`) cannot contain child steps.\n\n**Retry steps** re-execute from a target ancestor step for quality-control loops. Configure with `target_step_id` (ancestor step ID) and `max_retries` (1\u201310). Best practice: place a `gate` step before the retry to make retries conditional.\n\n**if_else** runs `then_steps` when its `conditions` (same shape as `gate`) match, otherwise its optional `else_steps`. Either branch's output flows to the if_else step's own `child_steps` (the post-branch continuation chain).\n\n**switch** dispatches on a `discriminator` template (default `{{input}}`) to the first matching case (equality by default; pass a list in `match` for `$in` semantics) or to `else_steps` when no case matches. Cases own their own `steps` subtrees; the chosen branch's output flows to the switch step's own `child_steps`.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. You can only update agents belonging to your account.", "operationId": "update_agent_definition_api_agents__agent_id__definition_put", "parameters": [ { @@ -10440,7 +10879,14 @@ "description": "Successful Response" }, "402": { - "description": "Insufficient credits \u2014 the account has exhausted its credits. The response body is `{\"detail\": {\"error\": \"insufficient_credits\", \"message\": ..., \"account_id\": ...}}`." + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InsufficientCreditsResponse" + } + } + }, + "description": "Insufficient credits \u2014 the account has exhausted its credits." }, "422": { "content": { @@ -10503,7 +10949,14 @@ "description": "Streams agent run events via Server-Sent Events (SSE); run is always created as priority.\n\nSSE events:\n- `event: init` \u2014 `data` is an `AgentRunResponse` snapshot (includes `run_id`).\n- `event: done` \u2014 `data` is the final `AgentRunResponse` snapshot (includes `output`, `credits`, etc).\n- Other events (e.g. `status`, step events) are forwarded from the run event stream.\n- On `timeout` / `error`, the payload includes `run_id` so clients can fetch status via `GET /api/agents/runs/{run_id}`." }, "402": { - "description": "Insufficient credits \u2014 the account has exhausted its credits. The response body is `{\"detail\": {\"error\": \"insufficient_credits\", \"message\": ..., \"account_id\": ...}}`." + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InsufficientCreditsResponse" + } + } + }, + "description": "Insufficient credits \u2014 the account has exhausted its credits." }, "422": { "content": { @@ -10583,7 +11036,7 @@ }, "/agents/{agent_id}/upload-input": { "post": { - "description": "Upload a file to use as input for a `dynamic_input` agent run.\n\nSupports the same file types as content source uploads: text, PDF, DOCX, audio, video, images, etc. Text and document files are processed synchronously; audio/video are submitted for asynchronous transcription.\n\n**Size limit:** 200 MB per file.\n\n**Supported extensions:** txt, html, md, csv, xml, json, pdf, msg, docx, doc, pptx, ppt, xlsx, xls, zip, epub, png, jpg, gif, bmp, tiff, webp, mp3, wav, m4a, flac, ogg, mp4, mov, avi.\n\nAfter uploading, poll `GET /agents/{agent_id}/input-uploads/{upload_id}` until `status` is `ready`, then pass `input_upload_id` to `POST /agents/{agent_id}/runs`.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. All resources are scoped to the caller's account.", + "description": "Upload a file to use as input for a `dynamic_input` agent run.\n\nSupports the same file types as content source uploads: text, PDF, DOCX, audio, video, images, etc. Text and document files are processed synchronously; audio/video are submitted for asynchronous transcription.\n\n**Size limit:** 200 MB per file.\n\n**Supported extensions:** txt, html, md, csv, xml, json, pdf, msg, docx, doc, pptx, ppt, xlsx, xls, zip, epub, png, jpg, gif, bmp, tiff, webp, mp3, wav, m4a, flac, ogg, mp4, mov, avi.\n\nAfter uploading, poll `GET /agents/{agent_id}/input-uploads/{upload_id}` until `status` is `ready`, then pass `input_upload_id` to `POST /agents/{agent_id}/runs`.\n\n**Multi-modal routing:** non-text uploads (image, audio, video, PDF) are surfaced natively to multi-modal-capable prompt steps; text-only models fall back to the OCR / transcript text counterpart. Audio originals are preserved past transcription so audio-capable models (Gemini 2.5/3, GPT-5 audio) read them directly; the agent-input-binary janitor sweeps originals once they pass your account's agent-trace retention (the agent-traces source's retention period; free default 7 days).\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. All resources are scoped to the caller's account.", "operationId": "api_upload_agent_input_api_agents__agent_id__upload_input_post", "parameters": [ { @@ -10687,7 +11140,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AiAssistantGenerateRequest" + "$ref": "#/components/schemas/routers__api__solutions__AiAssistantGenerateRequest" } } }, @@ -10906,7 +11359,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AiAssistantGenerateRequest" + "$ref": "#/components/schemas/routers__api__solutions__AiAssistantGenerateRequest" } } }, @@ -10953,7 +11406,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AiAssistantGenerateRequest" + "$ref": "#/components/schemas/routers__api__solutions__AiAssistantGenerateRequest" } } }, @@ -12142,7 +12595,7 @@ }, "/contents/{source_connection_content_version}/upload": { "post": { - "description": "Upload a new file and replace the content backing an existing `SourceConnectionContentVersion`.\n\nThis behaves like a source file upload, but it targets an existing content version ID. This is useful when you want to correct or update an uploaded document while keeping references stable.\n\n**Maximum file size:** 209715200 bytes.\n\n**Supported MIME types:**\n- `application/epub+zip`\n- `application/json`\n- `application/pdf`\n- `application/vnd.ms-excel`\n- `application/vnd.ms-outlook`\n- `application/vnd.ms-powerpoint`\n- `application/vnd.openxmlformats-officedocument.presentationml.presentation`\n- `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`\n- `application/vnd.openxmlformats-officedocument.wordprocessingml.document`\n- `application/xml`\n- `application/zip`\n- `audio/flac`\n- `audio/mp4`\n- `audio/mpeg`\n- `audio/ogg`\n- `audio/wav`\n- `image/bmp`\n- `image/gif`\n- `image/jpeg`\n- `image/png`\n- `image/tiff`\n- `image/webp`\n- `text/csv`\n- `text/html`\n- `text/markdown`\n- `text/plain`\n- `text/x-markdown`\n- `text/xml`\n- `video/mp4`\n- `video/quicktime`\n- `video/x-msvideo`\n\nNotes:\n- If the uploaded file's content type is `application/octet-stream`, the server attempts to infer the type from the file extension.\n- Use `metadata` to attach an arbitrary JSON object of metadata (for example `metadata={\"category\":\"docs\"}`).\n- `title` is a convenience field and is merged into the metadata as `metadata.title` (it does not override an existing `metadata.title`).\n- For backwards compatibility, you can also pass form fields named `metadata_` (for example `metadata_author=...`). These override keys from `metadata`.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. You can only replace content belonging to your account.", + "description": "Upload a new file and replace the content backing an existing `SourceConnectionContentVersion`.\n\nThis behaves like a source file upload, but it targets an existing content version ID. This is useful when you want to correct or update an uploaded document while keeping references stable.\n\n**Maximum file size:** 209715200 bytes.\n\n**Supported MIME types:**\n- `application/epub+zip`\n- `application/json`\n- `application/pdf`\n- `application/vnd.ms-excel`\n- `application/vnd.ms-outlook`\n- `application/vnd.ms-powerpoint`\n- `application/vnd.openxmlformats-officedocument.presentationml.presentation`\n- `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`\n- `application/vnd.openxmlformats-officedocument.wordprocessingml.document`\n- `application/xml`\n- `application/zip`\n- `audio/flac`\n- `audio/mp4`\n- `audio/mpeg`\n- `audio/ogg`\n- `audio/wav`\n- `image/bmp`\n- `image/gif`\n- `image/jpeg`\n- `image/png`\n- `image/svg+xml`\n- `image/tiff`\n- `image/webp`\n- `text/csv`\n- `text/html`\n- `text/markdown`\n- `text/plain`\n- `text/x-markdown`\n- `text/xml`\n- `video/mp4`\n- `video/quicktime`\n- `video/x-msvideo`\n\nNotes:\n- If the uploaded file's content type is `application/octet-stream`, the server attempts to infer the type from the file extension.\n- Use `metadata` to attach an arbitrary JSON object of metadata (for example `metadata={\"category\":\"docs\"}`).\n- `title` is a convenience field and is merged into the metadata as `metadata.title` (it does not override an existing `metadata.title`).\n- For backwards compatibility, you can also pass form fields named `metadata_` (for example `metadata_author=...`). These override keys from `metadata`.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. You can only replace content belonging to your account.", "operationId": "upload_file_to_content_api_contents__source_connection_content_version__upload_post", "parameters": [ { @@ -13984,6 +14437,44 @@ } }, "/models/playground/experiments/{experiment_id}": { + "delete": { + "description": "Soft-delete a model playground experiment.\n\nRemoves the experiment from list/detail views while preserving audit history.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth access token. The experiment must belong to the caller's account.", + "operationId": "delete_experiment_endpoint_api_models_playground_experiments__experiment_id__delete", + "parameters": [ + { + "in": "path", + "name": "experiment_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Experiment Id", + "type": "string" + } + }, + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Delete Experiment Endpoint", + "tags": [ + "models" + ] + }, "get": { "description": "Get details and results for a specific model playground experiment.\n\nReturns the full experiment payload including prompt, model outputs, and evaluation results (if available).\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. The experiment must belong to the caller's account.", "operationId": "get_experiment_api_models_playground_experiments__experiment_id__get", @@ -14776,7 +15267,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AiAssistantGenerateRequest" + "$ref": "#/components/schemas/routers__api__solutions__AiAssistantGenerateRequest" } } }, @@ -14833,7 +15324,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AiAssistantGenerateRequest" + "$ref": "#/components/schemas/routers__api__solutions__AiAssistantGenerateRequest" } } }, @@ -14890,7 +15381,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AiAssistantGenerateRequest" + "$ref": "#/components/schemas/routers__api__solutions__AiAssistantGenerateRequest" } } }, @@ -16344,7 +16835,7 @@ }, "/sources/{source_connection_id}/upload": { "post": { - "description": "Upload a file to a content source.\n\n**Maximum file size:** 209715200 bytes.\n\n**Supported MIME types:**\n- `application/epub+zip`\n- `application/json`\n- `application/pdf`\n- `application/vnd.ms-excel`\n- `application/vnd.ms-outlook`\n- `application/vnd.ms-powerpoint`\n- `application/vnd.openxmlformats-officedocument.presentationml.presentation`\n- `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`\n- `application/vnd.openxmlformats-officedocument.wordprocessingml.document`\n- `application/xml`\n- `application/zip`\n- `audio/flac`\n- `audio/mp4`\n- `audio/mpeg`\n- `audio/ogg`\n- `audio/wav`\n- `image/bmp`\n- `image/gif`\n- `image/jpeg`\n- `image/png`\n- `image/tiff`\n- `image/webp`\n- `text/csv`\n- `text/html`\n- `text/markdown`\n- `text/plain`\n- `text/x-markdown`\n- `text/xml`\n- `video/mp4`\n- `video/quicktime`\n- `video/x-msvideo`\n\nNotes:\n- If the uploaded file's content type is `application/octet-stream`, the server attempts to infer the type from the file extension.\n- Use `metadata` to attach an arbitrary JSON object of metadata (for example `metadata={\"author\":\"Ada\",\"category\":\"docs\"}`).\n- `title` is a convenience field and is merged into the metadata as `metadata.title` (it does not override an existing `metadata.title`).\n- For backwards compatibility, you can also pass form fields named `metadata_` (for example `metadata_author=...`). These override keys from `metadata`.\n\nResponse:\n- `status` is `uploaded` for a new upload, or `duplicate` when the same file already exists for this source.", + "description": "Upload a file to a content source.\n\n**Maximum file size:** 209715200 bytes.\n\n**Supported MIME types:**\n- `application/epub+zip`\n- `application/json`\n- `application/pdf`\n- `application/vnd.ms-excel`\n- `application/vnd.ms-outlook`\n- `application/vnd.ms-powerpoint`\n- `application/vnd.openxmlformats-officedocument.presentationml.presentation`\n- `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`\n- `application/vnd.openxmlformats-officedocument.wordprocessingml.document`\n- `application/xml`\n- `application/zip`\n- `audio/flac`\n- `audio/mp4`\n- `audio/mpeg`\n- `audio/ogg`\n- `audio/wav`\n- `image/bmp`\n- `image/gif`\n- `image/jpeg`\n- `image/png`\n- `image/svg+xml`\n- `image/tiff`\n- `image/webp`\n- `text/csv`\n- `text/html`\n- `text/markdown`\n- `text/plain`\n- `text/x-markdown`\n- `text/xml`\n- `video/mp4`\n- `video/quicktime`\n- `video/x-msvideo`\n\nNotes:\n- If the uploaded file's content type is `application/octet-stream`, the server attempts to infer the type from the file extension.\n- Use `metadata` to attach an arbitrary JSON object of metadata (for example `metadata={\"author\":\"Ada\",\"category\":\"docs\"}`).\n- `title` is a convenience field and is merged into the metadata as `metadata.title` (it does not override an existing `metadata.title`).\n- For backwards compatibility, you can also pass form fields named `metadata_` (for example `metadata_author=...`). These override keys from `metadata`.\n\nResponse:\n- `status` is `uploaded` for a new upload, or `duplicate` when the same file already exists for this source.", "operationId": "upload_file_to_source_api_sources__source_connection_id__upload_post", "parameters": [ { @@ -16397,6 +16888,77 @@ "sources" ] } + }, + "/v2/agent-runs/{run_id}/attachments/{attachment_id}": { + "get": { + "description": "Streams the bytes of an attachment emitted by a step in the given agent run. ``attachment_id`` is the URL-safe-base64-encoded ``storage_key`` (use the encoder shared by webhook + email payload builders).\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token.\n- The calling account must own ``run_id``; lookup failures (missing run, cross-account run, soft-deleted agent, unreferenced storage_key) all collapse to a single 404 to prevent cross-tenant existence enumeration.\n\nMIME handling:\n- Inline-safe MIMEs (image/*, audio/*, video/*, application/pdf, text/plain, application/vnd.seclai.manifest+json) are served with their declared type.\n- Everything else is served as ``application/octet-stream`` with an attachment disposition to prevent stored-XSS.", + "operationId": "serve_agent_run_attachment_api_v2_agent_runs__run_id__attachments__attachment_id__get", + "parameters": [ + { + "in": "path", + "name": "run_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Run Id", + "type": "string" + } + }, + { + "in": "path", + "name": "attachment_id", + "required": true, + "schema": { + "title": "Attachment Id", + "type": "string" + } + }, + { + "in": "query", + "name": "download_name", + "required": false, + "schema": { + "anyOf": [ + { + "maxLength": 255, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Download Name" + } + }, + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Download an agent-run attachment", + "tags": [ + "agent_run_attachments" + ] + } } }, "security": [ diff --git a/seclai/_generated/api/agent_run_attachments/__init__.py b/seclai/_generated/api/agent_run_attachments/__init__.py new file mode 100644 index 0000000..2d7c0b2 --- /dev/null +++ b/seclai/_generated/api/agent_run_attachments/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/seclai/_generated/api/agent_run_attachments/serve_agent_run_attachment_api_v2_agent_runs_run_id_attachments_attachment_id_get.py b/seclai/_generated/api/agent_run_attachments/serve_agent_run_attachment_api_v2_agent_runs_run_id_attachments_attachment_id_get.py new file mode 100644 index 0000000..565f8ef --- /dev/null +++ b/seclai/_generated/api/agent_run_attachments/serve_agent_run_attachment_api_v2_agent_runs_run_id_attachments_attachment_id_get.py @@ -0,0 +1,281 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote +from uuid import UUID + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + run_id: UUID, + attachment_id: str, + *, + download_name: None | str | Unset = UNSET, + x_account_id: UUID | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + if not isinstance(x_account_id, Unset): + headers["X-Account-Id"] = x_account_id + + params: dict[str, Any] = {} + + json_download_name: None | str | Unset + if isinstance(download_name, Unset): + json_download_name = UNSET + else: + json_download_name = download_name + params["download_name"] = json_download_name + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/v2/agent-runs/{run_id}/attachments/{attachment_id}".format( + run_id=quote(str(run_id), safe=""), + attachment_id=quote(str(attachment_id), safe=""), + ), + "params": params, + } + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Any | HTTPValidationError | None: + if response.status_code == 200: + response_200 = response.json() + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + run_id: UUID, + attachment_id: str, + *, + client: AuthenticatedClient | Client, + download_name: None | str | Unset = UNSET, + x_account_id: UUID | Unset = UNSET, +) -> Response[Any | HTTPValidationError]: + """Download an agent-run attachment + + Streams the bytes of an attachment emitted by a step in the given agent run. ``attachment_id`` is + the URL-safe-base64-encoded ``storage_key`` (use the encoder shared by webhook + email payload + builders). + + Auth & scoping: + - Requires `X-API-Key` header or OAuth Bearer token. + - The calling account must own ``run_id``; lookup failures (missing run, cross-account run, soft- + deleted agent, unreferenced storage_key) all collapse to a single 404 to prevent cross-tenant + existence enumeration. + + MIME handling: + - Inline-safe MIMEs (image/*, audio/*, video/*, application/pdf, text/plain, + application/vnd.seclai.manifest+json) are served with their declared type. + - Everything else is served as ``application/octet-stream`` with an attachment disposition to + prevent stored-XSS. + + Args: + run_id (UUID): + attachment_id (str): + download_name (None | str | Unset): + x_account_id (UUID | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | HTTPValidationError] + """ + + kwargs = _get_kwargs( + run_id=run_id, + attachment_id=attachment_id, + download_name=download_name, + x_account_id=x_account_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + run_id: UUID, + attachment_id: str, + *, + client: AuthenticatedClient | Client, + download_name: None | str | Unset = UNSET, + x_account_id: UUID | Unset = UNSET, +) -> Any | HTTPValidationError | None: + """Download an agent-run attachment + + Streams the bytes of an attachment emitted by a step in the given agent run. ``attachment_id`` is + the URL-safe-base64-encoded ``storage_key`` (use the encoder shared by webhook + email payload + builders). + + Auth & scoping: + - Requires `X-API-Key` header or OAuth Bearer token. + - The calling account must own ``run_id``; lookup failures (missing run, cross-account run, soft- + deleted agent, unreferenced storage_key) all collapse to a single 404 to prevent cross-tenant + existence enumeration. + + MIME handling: + - Inline-safe MIMEs (image/*, audio/*, video/*, application/pdf, text/plain, + application/vnd.seclai.manifest+json) are served with their declared type. + - Everything else is served as ``application/octet-stream`` with an attachment disposition to + prevent stored-XSS. + + Args: + run_id (UUID): + attachment_id (str): + download_name (None | str | Unset): + x_account_id (UUID | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | HTTPValidationError + """ + + return sync_detailed( + run_id=run_id, + attachment_id=attachment_id, + client=client, + download_name=download_name, + x_account_id=x_account_id, + ).parsed + + +async def asyncio_detailed( + run_id: UUID, + attachment_id: str, + *, + client: AuthenticatedClient | Client, + download_name: None | str | Unset = UNSET, + x_account_id: UUID | Unset = UNSET, +) -> Response[Any | HTTPValidationError]: + """Download an agent-run attachment + + Streams the bytes of an attachment emitted by a step in the given agent run. ``attachment_id`` is + the URL-safe-base64-encoded ``storage_key`` (use the encoder shared by webhook + email payload + builders). + + Auth & scoping: + - Requires `X-API-Key` header or OAuth Bearer token. + - The calling account must own ``run_id``; lookup failures (missing run, cross-account run, soft- + deleted agent, unreferenced storage_key) all collapse to a single 404 to prevent cross-tenant + existence enumeration. + + MIME handling: + - Inline-safe MIMEs (image/*, audio/*, video/*, application/pdf, text/plain, + application/vnd.seclai.manifest+json) are served with their declared type. + - Everything else is served as ``application/octet-stream`` with an attachment disposition to + prevent stored-XSS. + + Args: + run_id (UUID): + attachment_id (str): + download_name (None | str | Unset): + x_account_id (UUID | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | HTTPValidationError] + """ + + kwargs = _get_kwargs( + run_id=run_id, + attachment_id=attachment_id, + download_name=download_name, + x_account_id=x_account_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + run_id: UUID, + attachment_id: str, + *, + client: AuthenticatedClient | Client, + download_name: None | str | Unset = UNSET, + x_account_id: UUID | Unset = UNSET, +) -> Any | HTTPValidationError | None: + """Download an agent-run attachment + + Streams the bytes of an attachment emitted by a step in the given agent run. ``attachment_id`` is + the URL-safe-base64-encoded ``storage_key`` (use the encoder shared by webhook + email payload + builders). + + Auth & scoping: + - Requires `X-API-Key` header or OAuth Bearer token. + - The calling account must own ``run_id``; lookup failures (missing run, cross-account run, soft- + deleted agent, unreferenced storage_key) all collapse to a single 404 to prevent cross-tenant + existence enumeration. + + MIME handling: + - Inline-safe MIMEs (image/*, audio/*, video/*, application/pdf, text/plain, + application/vnd.seclai.manifest+json) are served with their declared type. + - Everything else is served as ``application/octet-stream`` with an attachment disposition to + prevent stored-XSS. + + Args: + run_id (UUID): + attachment_id (str): + download_name (None | str | Unset): + x_account_id (UUID | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | HTTPValidationError + """ + + return ( + await asyncio_detailed( + run_id=run_id, + attachment_id=attachment_id, + client=client, + download_name=download_name, + x_account_id=x_account_id, + ) + ).parsed diff --git a/seclai/_generated/api/agents/api_get_agent_attachment_references_api_agents_agent_id_attachment_references_get.py b/seclai/_generated/api/agents/api_get_agent_attachment_references_api_agents_agent_id_attachment_references_get.py new file mode 100644 index 0000000..85b196b --- /dev/null +++ b/seclai/_generated/api/agents/api_get_agent_attachment_references_api_agents_agent_id_attachment_references_get.py @@ -0,0 +1,208 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote +from uuid import UUID + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.agent_attachment_refs_api_response import AgentAttachmentRefsApiResponse +from ...models.http_validation_error import HTTPValidationError +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + agent_id: str, + *, + x_account_id: UUID | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + if not isinstance(x_account_id, Unset): + headers["X-Account-Id"] = x_account_id + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/agents/{agent_id}/attachment-references".format( + agent_id=quote(str(agent_id), safe=""), + ), + } + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> AgentAttachmentRefsApiResponse | HTTPValidationError | None: + if response.status_code == 200: + response_200 = AgentAttachmentRefsApiResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[AgentAttachmentRefsApiResponse | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + agent_id: str, + *, + client: AuthenticatedClient | Client, + x_account_id: UUID | Unset = UNSET, +) -> Response[AgentAttachmentRefsApiResponse | HTTPValidationError]: + """Get agent attachment-reference contract + + Return the static attachment-reference contract for an agent — what files the agent's definition + expects on a run. + + Call this BEFORE staging uploads so you know whether the agent accepts files at all + (``requires_uploads``), and which specific filenames/indexes/patterns the templates reference. + Mismatched batches are rejected at run time with HTTP 400. + + Args: + agent_id (str): + x_account_id (UUID | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[AgentAttachmentRefsApiResponse | HTTPValidationError] + """ + + kwargs = _get_kwargs( + agent_id=agent_id, + x_account_id=x_account_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + agent_id: str, + *, + client: AuthenticatedClient | Client, + x_account_id: UUID | Unset = UNSET, +) -> AgentAttachmentRefsApiResponse | HTTPValidationError | None: + """Get agent attachment-reference contract + + Return the static attachment-reference contract for an agent — what files the agent's definition + expects on a run. + + Call this BEFORE staging uploads so you know whether the agent accepts files at all + (``requires_uploads``), and which specific filenames/indexes/patterns the templates reference. + Mismatched batches are rejected at run time with HTTP 400. + + Args: + agent_id (str): + x_account_id (UUID | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + AgentAttachmentRefsApiResponse | HTTPValidationError + """ + + return sync_detailed( + agent_id=agent_id, + client=client, + x_account_id=x_account_id, + ).parsed + + +async def asyncio_detailed( + agent_id: str, + *, + client: AuthenticatedClient | Client, + x_account_id: UUID | Unset = UNSET, +) -> Response[AgentAttachmentRefsApiResponse | HTTPValidationError]: + """Get agent attachment-reference contract + + Return the static attachment-reference contract for an agent — what files the agent's definition + expects on a run. + + Call this BEFORE staging uploads so you know whether the agent accepts files at all + (``requires_uploads``), and which specific filenames/indexes/patterns the templates reference. + Mismatched batches are rejected at run time with HTTP 400. + + Args: + agent_id (str): + x_account_id (UUID | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[AgentAttachmentRefsApiResponse | HTTPValidationError] + """ + + kwargs = _get_kwargs( + agent_id=agent_id, + x_account_id=x_account_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + agent_id: str, + *, + client: AuthenticatedClient | Client, + x_account_id: UUID | Unset = UNSET, +) -> AgentAttachmentRefsApiResponse | HTTPValidationError | None: + """Get agent attachment-reference contract + + Return the static attachment-reference contract for an agent — what files the agent's definition + expects on a run. + + Call this BEFORE staging uploads so you know whether the agent accepts files at all + (``requires_uploads``), and which specific filenames/indexes/patterns the templates reference. + Mismatched batches are rejected at run time with HTTP 400. + + Args: + agent_id (str): + x_account_id (UUID | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + AgentAttachmentRefsApiResponse | HTTPValidationError + """ + + return ( + await asyncio_detailed( + agent_id=agent_id, + client=client, + x_account_id=x_account_id, + ) + ).parsed diff --git a/seclai/_generated/api/agents/api_upload_agent_input_api_agents_agent_id_upload_input_post.py b/seclai/_generated/api/agents/api_upload_agent_input_api_agents_agent_id_upload_input_post.py index 862c9b3..125e548 100644 --- a/seclai/_generated/api/agents/api_upload_agent_input_api_agents_agent_id_upload_input_post.py +++ b/seclai/_generated/api/agents/api_upload_agent_input_api_agents_agent_id_upload_input_post.py @@ -84,6 +84,12 @@ def sync_detailed( After uploading, poll `GET /agents/{agent_id}/input-uploads/{upload_id}` until `status` is `ready`, then pass `input_upload_id` to `POST /agents/{agent_id}/runs`. + **Multi-modal routing:** non-text uploads (image, audio, video, PDF) are surfaced natively to multi- + modal-capable prompt steps; text-only models fall back to the OCR / transcript text counterpart. + Audio originals are preserved past transcription so audio-capable models (Gemini 2.5/3, GPT-5 audio) + read them directly; the agent-input-binary janitor sweeps originals once they pass your account's + agent-trace retention (the agent-traces source's retention period; free default 7 days). + Auth & scoping: - Requires `X-API-Key` header or OAuth Bearer token. All resources are scoped to the caller's account. @@ -134,6 +140,12 @@ def sync( After uploading, poll `GET /agents/{agent_id}/input-uploads/{upload_id}` until `status` is `ready`, then pass `input_upload_id` to `POST /agents/{agent_id}/runs`. + **Multi-modal routing:** non-text uploads (image, audio, video, PDF) are surfaced natively to multi- + modal-capable prompt steps; text-only models fall back to the OCR / transcript text counterpart. + Audio originals are preserved past transcription so audio-capable models (Gemini 2.5/3, GPT-5 audio) + read them directly; the agent-input-binary janitor sweeps originals once they pass your account's + agent-trace retention (the agent-traces source's retention period; free default 7 days). + Auth & scoping: - Requires `X-API-Key` header or OAuth Bearer token. All resources are scoped to the caller's account. @@ -179,6 +191,12 @@ async def asyncio_detailed( After uploading, poll `GET /agents/{agent_id}/input-uploads/{upload_id}` until `status` is `ready`, then pass `input_upload_id` to `POST /agents/{agent_id}/runs`. + **Multi-modal routing:** non-text uploads (image, audio, video, PDF) are surfaced natively to multi- + modal-capable prompt steps; text-only models fall back to the OCR / transcript text counterpart. + Audio originals are preserved past transcription so audio-capable models (Gemini 2.5/3, GPT-5 audio) + read them directly; the agent-input-binary janitor sweeps originals once they pass your account's + agent-trace retention (the agent-traces source's retention period; free default 7 days). + Auth & scoping: - Requires `X-API-Key` header or OAuth Bearer token. All resources are scoped to the caller's account. @@ -227,6 +245,12 @@ async def asyncio( After uploading, poll `GET /agents/{agent_id}/input-uploads/{upload_id}` until `status` is `ready`, then pass `input_upload_id` to `POST /agents/{agent_id}/runs`. + **Multi-modal routing:** non-text uploads (image, audio, video, PDF) are surfaced natively to multi- + modal-capable prompt steps; text-only models fall back to the OCR / transcript text counterpart. + Audio originals are preserved past transcription so audio-capable models (Gemini 2.5/3, GPT-5 audio) + read them directly; the agent-input-binary janitor sweeps originals once they pass your account's + agent-trace retention (the agent-traces source's retention period; free default 7 days). + Auth & scoping: - Requires `X-API-Key` header or OAuth Bearer token. All resources are scoped to the caller's account. diff --git a/seclai/_generated/api/agents/get_agent_definition_api_agents_agent_id_definition_get.py b/seclai/_generated/api/agents/get_agent_definition_api_agents_agent_id_definition_get.py index 3220c16..52fcb13 100644 --- a/seclai/_generated/api/agents/get_agent_definition_api_agents_agent_id_definition_get.py +++ b/seclai/_generated/api/agents/get_agent_definition_api_agents_agent_id_definition_get.py @@ -113,6 +113,18 @@ def sync_detailed( - `merge`: Combine multiple inputs into a single templated output - `text`: Static text literal - `for_each`: Iterate a body over a list of items (body lives in `body[]`) + - `if_else`: Conditional dispatch. Evaluates `conditions` (same shape as `gate`) and runs + `then_steps` on match, otherwise the optional `else_steps`. The chosen branch's output flows to the + if_else step's own `child_steps` (post-branch continuation chain). **`display_result` and + `streaming_result` are not allowed inside `then_steps` / `else_steps`** — end each branch with a + content-producing step (e.g. `text`, `prompt_call`) and place the single `display_result` in + `child_steps`. + - `switch`: Single-discriminator dispatch. Renders `discriminator` (default `{{input}}`) and routes + to the first matching `cases[]` entry (equality by default; pass a list in `match` for `$in` + semantics) or to `else_steps` when nothing matches. The chosen case's output flows to the switch + step's own `child_steps`. **`display_result` and `streaming_result` are not allowed inside + `cases[].steps` or `else_steps`** — end each case with a content-producing step and place the single + `display_result` in `child_steps`. Auth & scoping: - Requires `X-API-Key` header or OAuth Bearer token. You can only access agents belonging to your @@ -193,6 +205,18 @@ def sync( - `merge`: Combine multiple inputs into a single templated output - `text`: Static text literal - `for_each`: Iterate a body over a list of items (body lives in `body[]`) + - `if_else`: Conditional dispatch. Evaluates `conditions` (same shape as `gate`) and runs + `then_steps` on match, otherwise the optional `else_steps`. The chosen branch's output flows to the + if_else step's own `child_steps` (post-branch continuation chain). **`display_result` and + `streaming_result` are not allowed inside `then_steps` / `else_steps`** — end each branch with a + content-producing step (e.g. `text`, `prompt_call`) and place the single `display_result` in + `child_steps`. + - `switch`: Single-discriminator dispatch. Renders `discriminator` (default `{{input}}`) and routes + to the first matching `cases[]` entry (equality by default; pass a list in `match` for `$in` + semantics) or to `else_steps` when nothing matches. The chosen case's output flows to the switch + step's own `child_steps`. **`display_result` and `streaming_result` are not allowed inside + `cases[].steps` or `else_steps`** — end each case with a content-producing step and place the single + `display_result` in `child_steps`. Auth & scoping: - Requires `X-API-Key` header or OAuth Bearer token. You can only access agents belonging to your @@ -268,6 +292,18 @@ async def asyncio_detailed( - `merge`: Combine multiple inputs into a single templated output - `text`: Static text literal - `for_each`: Iterate a body over a list of items (body lives in `body[]`) + - `if_else`: Conditional dispatch. Evaluates `conditions` (same shape as `gate`) and runs + `then_steps` on match, otherwise the optional `else_steps`. The chosen branch's output flows to the + if_else step's own `child_steps` (post-branch continuation chain). **`display_result` and + `streaming_result` are not allowed inside `then_steps` / `else_steps`** — end each branch with a + content-producing step (e.g. `text`, `prompt_call`) and place the single `display_result` in + `child_steps`. + - `switch`: Single-discriminator dispatch. Renders `discriminator` (default `{{input}}`) and routes + to the first matching `cases[]` entry (equality by default; pass a list in `match` for `$in` + semantics) or to `else_steps` when nothing matches. The chosen case's output flows to the switch + step's own `child_steps`. **`display_result` and `streaming_result` are not allowed inside + `cases[].steps` or `else_steps`** — end each case with a content-producing step and place the single + `display_result` in `child_steps`. Auth & scoping: - Requires `X-API-Key` header or OAuth Bearer token. You can only access agents belonging to your @@ -346,6 +382,18 @@ async def asyncio( - `merge`: Combine multiple inputs into a single templated output - `text`: Static text literal - `for_each`: Iterate a body over a list of items (body lives in `body[]`) + - `if_else`: Conditional dispatch. Evaluates `conditions` (same shape as `gate`) and runs + `then_steps` on match, otherwise the optional `else_steps`. The chosen branch's output flows to the + if_else step's own `child_steps` (post-branch continuation chain). **`display_result` and + `streaming_result` are not allowed inside `then_steps` / `else_steps`** — end each branch with a + content-producing step (e.g. `text`, `prompt_call`) and place the single `display_result` in + `child_steps`. + - `switch`: Single-discriminator dispatch. Renders `discriminator` (default `{{input}}`) and routes + to the first matching `cases[]` entry (equality by default; pass a list in `match` for `$in` + semantics) or to `else_steps` when nothing matches. The chosen case's output flows to the switch + step's own `child_steps`. **`display_result` and `streaming_result` are not allowed inside + `cases[].steps` or `else_steps`** — end each case with a content-producing step and place the single + `display_result` in `child_steps`. Auth & scoping: - Requires `X-API-Key` header or OAuth Bearer token. You can only access agents belonging to your diff --git a/seclai/_generated/api/agents/run_agent_api_agents_agent_id_runs_post.py b/seclai/_generated/api/agents/run_agent_api_agents_agent_id_runs_post.py index 168ee04..d8484cf 100644 --- a/seclai/_generated/api/agents/run_agent_api_agents_agent_id_runs_post.py +++ b/seclai/_generated/api/agents/run_agent_api_agents_agent_id_runs_post.py @@ -1,5 +1,5 @@ from http import HTTPStatus -from typing import Any, cast +from typing import Any from urllib.parse import quote from uuid import UUID @@ -10,6 +10,7 @@ from ...models.agent_run_request import AgentRunRequest from ...models.agent_run_response import AgentRunResponse from ...models.http_validation_error import HTTPValidationError +from ...models.insufficient_credits_response import InsufficientCreditsResponse from ...types import UNSET, Response, Unset @@ -40,14 +41,15 @@ def _get_kwargs( def _parse_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> AgentRunResponse | Any | HTTPValidationError | None: +) -> AgentRunResponse | HTTPValidationError | InsufficientCreditsResponse | None: if response.status_code == 200: response_200 = AgentRunResponse.from_dict(response.json()) return response_200 if response.status_code == 402: - response_402 = cast(Any, None) + response_402 = InsufficientCreditsResponse.from_dict(response.json()) + return response_402 if response.status_code == 422: @@ -63,7 +65,7 @@ def _parse_response( def _build_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[AgentRunResponse | Any | HTTPValidationError]: +) -> Response[AgentRunResponse | HTTPValidationError | InsufficientCreditsResponse]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -78,7 +80,7 @@ def sync_detailed( client: AuthenticatedClient | Client, body: AgentRunRequest, x_account_id: UUID | Unset = UNSET, -) -> Response[AgentRunResponse | Any | HTTPValidationError]: +) -> Response[AgentRunResponse | HTTPValidationError | InsufficientCreditsResponse]: """Run an agent Start an agent run. @@ -118,7 +120,7 @@ def sync_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[AgentRunResponse | Any | HTTPValidationError] + Response[AgentRunResponse | HTTPValidationError | InsufficientCreditsResponse] """ kwargs = _get_kwargs( @@ -140,7 +142,7 @@ def sync( client: AuthenticatedClient | Client, body: AgentRunRequest, x_account_id: UUID | Unset = UNSET, -) -> AgentRunResponse | Any | HTTPValidationError | None: +) -> AgentRunResponse | HTTPValidationError | InsufficientCreditsResponse | None: """Run an agent Start an agent run. @@ -180,7 +182,7 @@ def sync( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - AgentRunResponse | Any | HTTPValidationError + AgentRunResponse | HTTPValidationError | InsufficientCreditsResponse """ return sync_detailed( @@ -197,7 +199,7 @@ async def asyncio_detailed( client: AuthenticatedClient | Client, body: AgentRunRequest, x_account_id: UUID | Unset = UNSET, -) -> Response[AgentRunResponse | Any | HTTPValidationError]: +) -> Response[AgentRunResponse | HTTPValidationError | InsufficientCreditsResponse]: """Run an agent Start an agent run. @@ -237,7 +239,7 @@ async def asyncio_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[AgentRunResponse | Any | HTTPValidationError] + Response[AgentRunResponse | HTTPValidationError | InsufficientCreditsResponse] """ kwargs = _get_kwargs( @@ -257,7 +259,7 @@ async def asyncio( client: AuthenticatedClient | Client, body: AgentRunRequest, x_account_id: UUID | Unset = UNSET, -) -> AgentRunResponse | Any | HTTPValidationError | None: +) -> AgentRunResponse | HTTPValidationError | InsufficientCreditsResponse | None: """Run an agent Start an agent run. @@ -297,7 +299,7 @@ async def asyncio( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - AgentRunResponse | Any | HTTPValidationError + AgentRunResponse | HTTPValidationError | InsufficientCreditsResponse """ return ( diff --git a/seclai/_generated/api/agents/run_streaming_agent_api_agents_agent_id_runs_stream_post.py b/seclai/_generated/api/agents/run_streaming_agent_api_agents_agent_id_runs_stream_post.py index 9b12bfb..7c437d7 100644 --- a/seclai/_generated/api/agents/run_streaming_agent_api_agents_agent_id_runs_stream_post.py +++ b/seclai/_generated/api/agents/run_streaming_agent_api_agents_agent_id_runs_stream_post.py @@ -1,5 +1,5 @@ from http import HTTPStatus -from typing import Any, cast +from typing import Any from urllib.parse import quote from uuid import UUID @@ -9,6 +9,7 @@ from ...client import AuthenticatedClient, Client from ...models.agent_run_stream_request import AgentRunStreamRequest from ...models.http_validation_error import HTTPValidationError +from ...models.insufficient_credits_response import InsufficientCreditsResponse from ...types import UNSET, Response, Unset @@ -39,13 +40,14 @@ def _get_kwargs( def _parse_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Any | HTTPValidationError | None: +) -> Any | HTTPValidationError | InsufficientCreditsResponse | None: if response.status_code == 200: response_200 = response.json() return response_200 if response.status_code == 402: - response_402 = cast(Any, None) + response_402 = InsufficientCreditsResponse.from_dict(response.json()) + return response_402 if response.status_code == 422: @@ -61,7 +63,7 @@ def _parse_response( def _build_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[Any | HTTPValidationError]: +) -> Response[Any | HTTPValidationError | InsufficientCreditsResponse]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -76,7 +78,7 @@ def sync_detailed( client: AuthenticatedClient | Client, body: AgentRunStreamRequest, x_account_id: UUID | Unset = UNSET, -) -> Response[Any | HTTPValidationError]: +) -> Response[Any | HTTPValidationError | InsufficientCreditsResponse]: """Run an agent (stream events) Start a **priority** agent run and stream run events using Server-Sent Events (SSE). @@ -115,7 +117,7 @@ def sync_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Any | HTTPValidationError] + Response[Any | HTTPValidationError | InsufficientCreditsResponse] """ kwargs = _get_kwargs( @@ -137,7 +139,7 @@ def sync( client: AuthenticatedClient | Client, body: AgentRunStreamRequest, x_account_id: UUID | Unset = UNSET, -) -> Any | HTTPValidationError | None: +) -> Any | HTTPValidationError | InsufficientCreditsResponse | None: """Run an agent (stream events) Start a **priority** agent run and stream run events using Server-Sent Events (SSE). @@ -176,7 +178,7 @@ def sync( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Any | HTTPValidationError + Any | HTTPValidationError | InsufficientCreditsResponse """ return sync_detailed( @@ -193,7 +195,7 @@ async def asyncio_detailed( client: AuthenticatedClient | Client, body: AgentRunStreamRequest, x_account_id: UUID | Unset = UNSET, -) -> Response[Any | HTTPValidationError]: +) -> Response[Any | HTTPValidationError | InsufficientCreditsResponse]: """Run an agent (stream events) Start a **priority** agent run and stream run events using Server-Sent Events (SSE). @@ -232,7 +234,7 @@ async def asyncio_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Any | HTTPValidationError] + Response[Any | HTTPValidationError | InsufficientCreditsResponse] """ kwargs = _get_kwargs( @@ -252,7 +254,7 @@ async def asyncio( client: AuthenticatedClient | Client, body: AgentRunStreamRequest, x_account_id: UUID | Unset = UNSET, -) -> Any | HTTPValidationError | None: +) -> Any | HTTPValidationError | InsufficientCreditsResponse | None: """Run an agent (stream events) Start a **priority** agent run and stream run events using Server-Sent Events (SSE). @@ -291,7 +293,7 @@ async def asyncio( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Any | HTTPValidationError + Any | HTTPValidationError | InsufficientCreditsResponse """ return ( diff --git a/seclai/_generated/api/agents/update_agent_definition_api_agents_agent_id_definition_put.py b/seclai/_generated/api/agents/update_agent_definition_api_agents_agent_id_definition_put.py index b678663..8dcbf62 100644 --- a/seclai/_generated/api/agents/update_agent_definition_api_agents_agent_id_definition_put.py +++ b/seclai/_generated/api/agents/update_agent_definition_api_agents_agent_id_definition_put.py @@ -88,13 +88,22 @@ def sync_detailed( `add_chat_turn`, `load_chat_history`, `add_memory`, `search_memory`, `load_memory`, `streaming_result`, `send_email`, `webhook_call`, `write_aws_s3_object`, `call_agent`, `write_metadata`, `write_content_attachment`, `load_content_attachment`, `load_content`, - `display_result`, `join`, `merge`, `text`, and `for_each`. Non-composite step types - (`display_result`, `join`, `retry`, `streaming_result`) cannot contain child steps. + `display_result`, `join`, `merge`, `text`, `for_each`, `if_else`, and `switch`. Non-composite step + types (`display_result`, `join`, `retry`, `streaming_result`) cannot contain child steps. **Retry steps** re-execute from a target ancestor step for quality-control loops. Configure with `target_step_id` (ancestor step ID) and `max_retries` (1–10). Best practice: place a `gate` step before the retry to make retries conditional. + **if_else** runs `then_steps` when its `conditions` (same shape as `gate`) match, otherwise its + optional `else_steps`. Either branch's output flows to the if_else step's own `child_steps` (the + post-branch continuation chain). + + **switch** dispatches on a `discriminator` template (default `{{input}}`) to the first matching case + (equality by default; pass a list in `match` for `$in` semantics) or to `else_steps` when no case + matches. Cases own their own `steps` subtrees; the chosen branch's output flows to the switch step's + own `child_steps`. + Auth & scoping: - Requires `X-API-Key` header or OAuth Bearer token. You can only update agents belonging to your account. @@ -145,13 +154,22 @@ def sync( `add_chat_turn`, `load_chat_history`, `add_memory`, `search_memory`, `load_memory`, `streaming_result`, `send_email`, `webhook_call`, `write_aws_s3_object`, `call_agent`, `write_metadata`, `write_content_attachment`, `load_content_attachment`, `load_content`, - `display_result`, `join`, `merge`, `text`, and `for_each`. Non-composite step types - (`display_result`, `join`, `retry`, `streaming_result`) cannot contain child steps. + `display_result`, `join`, `merge`, `text`, `for_each`, `if_else`, and `switch`. Non-composite step + types (`display_result`, `join`, `retry`, `streaming_result`) cannot contain child steps. **Retry steps** re-execute from a target ancestor step for quality-control loops. Configure with `target_step_id` (ancestor step ID) and `max_retries` (1–10). Best practice: place a `gate` step before the retry to make retries conditional. + **if_else** runs `then_steps` when its `conditions` (same shape as `gate`) match, otherwise its + optional `else_steps`. Either branch's output flows to the if_else step's own `child_steps` (the + post-branch continuation chain). + + **switch** dispatches on a `discriminator` template (default `{{input}}`) to the first matching case + (equality by default; pass a list in `match` for `$in` semantics) or to `else_steps` when no case + matches. Cases own their own `steps` subtrees; the chosen branch's output flows to the switch step's + own `child_steps`. + Auth & scoping: - Requires `X-API-Key` header or OAuth Bearer token. You can only update agents belonging to your account. @@ -197,13 +215,22 @@ async def asyncio_detailed( `add_chat_turn`, `load_chat_history`, `add_memory`, `search_memory`, `load_memory`, `streaming_result`, `send_email`, `webhook_call`, `write_aws_s3_object`, `call_agent`, `write_metadata`, `write_content_attachment`, `load_content_attachment`, `load_content`, - `display_result`, `join`, `merge`, `text`, and `for_each`. Non-composite step types - (`display_result`, `join`, `retry`, `streaming_result`) cannot contain child steps. + `display_result`, `join`, `merge`, `text`, `for_each`, `if_else`, and `switch`. Non-composite step + types (`display_result`, `join`, `retry`, `streaming_result`) cannot contain child steps. **Retry steps** re-execute from a target ancestor step for quality-control loops. Configure with `target_step_id` (ancestor step ID) and `max_retries` (1–10). Best practice: place a `gate` step before the retry to make retries conditional. + **if_else** runs `then_steps` when its `conditions` (same shape as `gate`) match, otherwise its + optional `else_steps`. Either branch's output flows to the if_else step's own `child_steps` (the + post-branch continuation chain). + + **switch** dispatches on a `discriminator` template (default `{{input}}`) to the first matching case + (equality by default; pass a list in `match` for `$in` semantics) or to `else_steps` when no case + matches. Cases own their own `steps` subtrees; the chosen branch's output flows to the switch step's + own `child_steps`. + Auth & scoping: - Requires `X-API-Key` header or OAuth Bearer token. You can only update agents belonging to your account. @@ -252,13 +279,22 @@ async def asyncio( `add_chat_turn`, `load_chat_history`, `add_memory`, `search_memory`, `load_memory`, `streaming_result`, `send_email`, `webhook_call`, `write_aws_s3_object`, `call_agent`, `write_metadata`, `write_content_attachment`, `load_content_attachment`, `load_content`, - `display_result`, `join`, `merge`, `text`, and `for_each`. Non-composite step types - (`display_result`, `join`, `retry`, `streaming_result`) cannot contain child steps. + `display_result`, `join`, `merge`, `text`, `for_each`, `if_else`, and `switch`. Non-composite step + types (`display_result`, `join`, `retry`, `streaming_result`) cannot contain child steps. **Retry steps** re-execute from a target ancestor step for quality-control loops. Configure with `target_step_id` (ancestor step ID) and `max_retries` (1–10). Best practice: place a `gate` step before the retry to make retries conditional. + **if_else** runs `then_steps` when its `conditions` (same shape as `gate`) match, otherwise its + optional `else_steps`. Either branch's output flows to the if_else step's own `child_steps` (the + post-branch continuation chain). + + **switch** dispatches on a `discriminator` template (default `{{input}}`) to the first matching case + (equality by default; pass a list in `match` for `$in` semantics) or to `else_steps` when no case + matches. Cases own their own `steps` subtrees; the chosen branch's output flows to the switch step's + own `child_steps`. + Auth & scoping: - Requires `X-API-Key` header or OAuth Bearer token. You can only update agents belonging to your account. diff --git a/seclai/_generated/api/contents/upload_file_to_content_api_contents_source_connection_content_version_upload_post.py b/seclai/_generated/api/contents/upload_file_to_content_api_contents_source_connection_content_version_upload_post.py index 596b7a1..b935cd2 100644 --- a/seclai/_generated/api/contents/upload_file_to_content_api_contents_source_connection_content_version_upload_post.py +++ b/seclai/_generated/api/contents/upload_file_to_content_api_contents_source_connection_content_version_upload_post.py @@ -107,6 +107,7 @@ def sync_detailed( - `image/gif` - `image/jpeg` - `image/png` + - `image/svg+xml` - `image/tiff` - `image/webp` - `text/csv` @@ -196,6 +197,7 @@ def sync( - `image/gif` - `image/jpeg` - `image/png` + - `image/svg+xml` - `image/tiff` - `image/webp` - `text/csv` @@ -280,6 +282,7 @@ async def asyncio_detailed( - `image/gif` - `image/jpeg` - `image/png` + - `image/svg+xml` - `image/tiff` - `image/webp` - `text/csv` @@ -367,6 +370,7 @@ async def asyncio( - `image/gif` - `image/jpeg` - `image/png` + - `image/svg+xml` - `image/tiff` - `image/webp` - `text/csv` diff --git a/seclai/_generated/api/models/delete_experiment_endpoint_api_models_playground_experiments_experiment_id_delete.py b/seclai/_generated/api/models/delete_experiment_endpoint_api_models_playground_experiments_experiment_id_delete.py new file mode 100644 index 0000000..ee6139f --- /dev/null +++ b/seclai/_generated/api/models/delete_experiment_endpoint_api_models_playground_experiments_experiment_id_delete.py @@ -0,0 +1,210 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote +from uuid import UUID + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.http_validation_error import HTTPValidationError +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + experiment_id: UUID, + *, + x_account_id: UUID | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + if not isinstance(x_account_id, Unset): + headers["X-Account-Id"] = x_account_id + + _kwargs: dict[str, Any] = { + "method": "delete", + "url": "/models/playground/experiments/{experiment_id}".format( + experiment_id=quote(str(experiment_id), safe=""), + ), + } + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Any | HTTPValidationError | None: + if response.status_code == 204: + response_204 = cast(Any, None) + return response_204 + + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | HTTPValidationError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + experiment_id: UUID, + *, + client: AuthenticatedClient | Client, + x_account_id: UUID | Unset = UNSET, +) -> Response[Any | HTTPValidationError]: + """Delete Experiment Endpoint + + Soft-delete a model playground experiment. + + Removes the experiment from list/detail views while preserving audit history. + + Auth & scoping: + - Requires `X-API-Key` header or OAuth access token. The experiment must belong to the caller's + account. + + Args: + experiment_id (UUID): + x_account_id (UUID | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | HTTPValidationError] + """ + + kwargs = _get_kwargs( + experiment_id=experiment_id, + x_account_id=x_account_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + experiment_id: UUID, + *, + client: AuthenticatedClient | Client, + x_account_id: UUID | Unset = UNSET, +) -> Any | HTTPValidationError | None: + """Delete Experiment Endpoint + + Soft-delete a model playground experiment. + + Removes the experiment from list/detail views while preserving audit history. + + Auth & scoping: + - Requires `X-API-Key` header or OAuth access token. The experiment must belong to the caller's + account. + + Args: + experiment_id (UUID): + x_account_id (UUID | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | HTTPValidationError + """ + + return sync_detailed( + experiment_id=experiment_id, + client=client, + x_account_id=x_account_id, + ).parsed + + +async def asyncio_detailed( + experiment_id: UUID, + *, + client: AuthenticatedClient | Client, + x_account_id: UUID | Unset = UNSET, +) -> Response[Any | HTTPValidationError]: + """Delete Experiment Endpoint + + Soft-delete a model playground experiment. + + Removes the experiment from list/detail views while preserving audit history. + + Auth & scoping: + - Requires `X-API-Key` header or OAuth access token. The experiment must belong to the caller's + account. + + Args: + experiment_id (UUID): + x_account_id (UUID | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | HTTPValidationError] + """ + + kwargs = _get_kwargs( + experiment_id=experiment_id, + x_account_id=x_account_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + experiment_id: UUID, + *, + client: AuthenticatedClient | Client, + x_account_id: UUID | Unset = UNSET, +) -> Any | HTTPValidationError | None: + """Delete Experiment Endpoint + + Soft-delete a model playground experiment. + + Removes the experiment from list/detail views while preserving audit history. + + Auth & scoping: + - Requires `X-API-Key` header or OAuth access token. The experiment must belong to the caller's + account. + + Args: + experiment_id (UUID): + x_account_id (UUID | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | HTTPValidationError + """ + + return ( + await asyncio_detailed( + experiment_id=experiment_id, + client=client, + x_account_id=x_account_id, + ) + ).parsed diff --git a/seclai/_generated/api/sources/upload_file_to_source_api_sources_source_connection_id_upload_post.py b/seclai/_generated/api/sources/upload_file_to_source_api_sources_source_connection_id_upload_post.py index 56b7c9e..71a6604 100644 --- a/seclai/_generated/api/sources/upload_file_to_source_api_sources_source_connection_id_upload_post.py +++ b/seclai/_generated/api/sources/upload_file_to_source_api_sources_source_connection_id_upload_post.py @@ -96,6 +96,7 @@ def sync_detailed( - `image/gif` - `image/jpeg` - `image/png` + - `image/svg+xml` - `image/tiff` - `image/webp` - `text/csv` @@ -182,6 +183,7 @@ def sync( - `image/gif` - `image/jpeg` - `image/png` + - `image/svg+xml` - `image/tiff` - `image/webp` - `text/csv` @@ -263,6 +265,7 @@ async def asyncio_detailed( - `image/gif` - `image/jpeg` - `image/png` + - `image/svg+xml` - `image/tiff` - `image/webp` - `text/csv` @@ -347,6 +350,7 @@ async def asyncio( - `image/gif` - `image/jpeg` - `image/png` + - `image/svg+xml` - `image/tiff` - `image/webp` - `text/csv` diff --git a/seclai/_generated/models/__init__.py b/seclai/_generated/models/__init__.py index 0a1b7a4..e748ecb 100644 --- a/seclai/_generated/models/__init__.py +++ b/seclai/_generated/models/__init__.py @@ -8,6 +8,7 @@ from .add_conversation_turn_request_actions_taken_type_0 import ( AddConversationTurnRequestActionsTakenType0, ) +from .agent_attachment_refs_api_response import AgentAttachmentRefsApiResponse from .agent_definition_import_error_response import AgentDefinitionImportErrorResponse from .agent_definition_response import AgentDefinitionResponse from .agent_definition_response_definition import AgentDefinitionResponseDefinition @@ -47,6 +48,7 @@ from .agent_run_step_response import AgentRunStepResponse from .agent_run_stream_request import AgentRunStreamRequest from .agent_run_stream_request_metadata_type_0 import AgentRunStreamRequestMetadataType0 +from .agent_run_tool_call_response import AgentRunToolCallResponse from .agent_summary_response import AgentSummaryResponse from .agent_summary_response_sampling_config_type_0 import ( AgentSummaryResponseSamplingConfigType0, @@ -72,6 +74,7 @@ ApiAiMemoryBankAcceptApiAiAssistantMemoryBankConversationIdPatchResponseApiAiMemoryBankAcceptApiAiAssistantMemoryBankConversationIdPatch, ) from .applied_action_response import AppliedActionResponse +from .attachment_refs_source_api_summary import AttachmentRefsSourceApiSummary from .body_upload_file_to_content_api_contents_source_connection_content_version_upload_post import ( BodyUploadFileToContentApiContentsSourceConnectionContentVersionUploadPost, ) @@ -229,6 +232,8 @@ from .inline_text_upload_request_metadata_type_0 import ( InlineTextUploadRequestMetadataType0, ) +from .insufficient_credits_detail import InsufficientCreditsDetail +from .insufficient_credits_response import InsufficientCreditsResponse from .knowledge_base import KnowledgeBase from .knowledge_base_list_response_model import KnowledgeBaseListResponseModel from .link_resources_request import LinkResourcesRequest @@ -270,6 +275,7 @@ ) from .memory_bank_last_conversation_response import MemoryBankLastConversationResponse from .memory_bank_list_response_model import MemoryBankListResponseModel +from .modality_rate_response import ModalityRateResponse from .non_manual_evaluation_mode_stat_response import ( NonManualEvaluationModeStatResponse, ) @@ -378,6 +384,7 @@ "AddCommentRequest", "AddConversationTurnRequest", "AddConversationTurnRequestActionsTakenType0", + "AgentAttachmentRefsApiResponse", "AgentDefinitionImportErrorResponse", "AgentDefinitionResponse", "AgentDefinitionResponseDefinition", @@ -403,6 +410,7 @@ "AgentRunStepResponse", "AgentRunStreamRequest", "AgentRunStreamRequestMetadataType0", + "AgentRunToolCallResponse", "AgentSummaryResponse", "AgentSummaryResponseSamplingConfigType0", "AgentTraceMatchResponse", @@ -420,6 +428,7 @@ "AiConversationTurnResponseResultingConfigType0", "ApiAiMemoryBankAcceptApiAiAssistantMemoryBankConversationIdPatchResponseApiAiMemoryBankAcceptApiAiAssistantMemoryBankConversationIdPatch", "AppliedActionResponse", + "AttachmentRefsSourceApiSummary", "BodyUploadFileToContentApiContentsSourceConnectionContentVersionUploadPost", "BodyUploadFileToSourceApiSourcesSourceConnectionIdUploadPost", "CancelExperimentEndpointApiModelsPlaygroundExperimentsExperimentIdCancelPostResponseCancelExperimentEndpointApiModelsPlaygroundExperimentsExperimentIdCancelPost", @@ -505,6 +514,8 @@ "InlineTextReplaceRequestMetadataType0", "InlineTextUploadRequest", "InlineTextUploadRequestMetadataType0", + "InsufficientCreditsDetail", + "InsufficientCreditsResponse", "KnowledgeBase", "KnowledgeBaseListResponseModel", "LinkResourcesRequest", @@ -528,6 +539,7 @@ "MemoryBankLastConversationResponse", "MemoryBankListResponseModel", "MeResponse", + "ModalityRateResponse", "NonManualEvaluationModeStatResponse", "NonManualEvaluationSummaryResponse", "OrganizationAlertPreferenceListResponse", diff --git a/seclai/_generated/models/agent_attachment_refs_api_response.py b/seclai/_generated/models/agent_attachment_refs_api_response.py new file mode 100644 index 0000000..5df7f52 --- /dev/null +++ b/seclai/_generated/models/agent_attachment_refs_api_response.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.attachment_refs_source_api_summary import ( + AttachmentRefsSourceApiSummary, + ) + + +T = TypeVar("T", bound="AgentAttachmentRefsApiResponse") + + +@_attrs_define +class AgentAttachmentRefsApiResponse: + """Static attachment-reference contract for an agent. + + Mirrors the MCP ``get_agent_attachment_references`` tool: returns + what files (if any) an agent's templates expect on a run so API + consumers can stage uploads correctly before calling + ``POST /agents/{id}/runs``. + + Attributes: + requires_uploads (bool): When ``false`` the agent's definition does NOT reference any uploaded attachments — + ``POST /agents/{id}/upload-input`` will reject with HTTP 400. When ``true`` the ``agent`` block lists the + specific selectors a run-time batch must satisfy. + agent (AttachmentRefsSourceApiSummary | Unset): Per-source attachment-reference summary. + """ + + requires_uploads: bool + agent: AttachmentRefsSourceApiSummary | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + requires_uploads = self.requires_uploads + + agent: dict[str, Any] | Unset = UNSET + if not isinstance(self.agent, Unset): + agent = self.agent.to_dict() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "requires_uploads": requires_uploads, + } + ) + if agent is not UNSET: + field_dict["agent"] = agent + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.attachment_refs_source_api_summary import ( + AttachmentRefsSourceApiSummary, + ) + + d = dict(src_dict) + requires_uploads = d.pop("requires_uploads") + + _agent = d.pop("agent", UNSET) + agent: AttachmentRefsSourceApiSummary | Unset + if isinstance(_agent, Unset): + agent = UNSET + else: + agent = AttachmentRefsSourceApiSummary.from_dict(_agent) + + agent_attachment_refs_api_response = cls( + requires_uploads=requires_uploads, + agent=agent, + ) + + agent_attachment_refs_api_response.additional_properties = d + return agent_attachment_refs_api_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/seclai/_generated/models/agent_definition_import_error_response.py b/seclai/_generated/models/agent_definition_import_error_response.py index 366a772..c4829e4 100644 --- a/seclai/_generated/models/agent_definition_import_error_response.py +++ b/seclai/_generated/models/agent_definition_import_error_response.py @@ -1,13 +1,11 @@ from __future__ import annotations from collections.abc import Mapping -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast from attrs import define as _attrs_define from attrs import field as _attrs_field -from ..types import UNSET, Unset - if TYPE_CHECKING: from ..models.import_field_error_model import ImportFieldErrorModel @@ -19,22 +17,24 @@ class AgentDefinitionImportErrorResponse: """422 body for invalid `agent_definition` payloads. - Mirrors :py:meth:`AgentDefinitionImportError.to_response_dict`. + Mirrors `AgentDefinitionImportError.to_response_dict`. Attributes: + error (Literal['invalid_agent_definition']): Stable machine-readable error code. errors (list[ImportFieldErrorModel]): message (str): source (str): Canonical pretty-printed echo of the supplied payload — error line/column refer to this string. - error (str | Unset): Default: 'invalid_agent_definition'. """ + error: Literal["invalid_agent_definition"] errors: list[ImportFieldErrorModel] message: str source: str - error: str | Unset = "invalid_agent_definition" additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + error = self.error + errors = [] for errors_item_data in self.errors: errors_item = errors_item_data.to_dict() @@ -44,19 +44,16 @@ def to_dict(self) -> dict[str, Any]: source = self.source - error = self.error - field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( { + "error": error, "errors": errors, "message": message, "source": source, } ) - if error is not UNSET: - field_dict["error"] = error return field_dict @@ -65,6 +62,12 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.import_field_error_model import ImportFieldErrorModel d = dict(src_dict) + error = cast(Literal["invalid_agent_definition"], d.pop("error")) + if error != "invalid_agent_definition": + raise ValueError( + f"error must match const 'invalid_agent_definition', got '{error}'" + ) + errors = [] _errors = d.pop("errors") for errors_item_data in _errors: @@ -76,13 +79,11 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: source = d.pop("source") - error = d.pop("error", UNSET) - agent_definition_import_error_response = cls( + error=error, errors=errors, message=message, source=source, - error=error, ) agent_definition_import_error_response.additional_properties = d diff --git a/seclai/_generated/models/agent_definition_response.py b/seclai/_generated/models/agent_definition_response.py index 702b8b3..e49f7e0 100644 --- a/seclai/_generated/models/agent_definition_response.py +++ b/seclai/_generated/models/agent_definition_response.py @@ -29,7 +29,7 @@ class AgentDefinitionResponse: step workflow tree. Step types include prompt_call, retrieval, regex_replace, gate, retry, evaluate_step, extract_data, extract_content, add_chat_turn, load_chat_history, add_memory, search_memory, load_memory, streaming_result, send_email, webhook_call, call_agent, write_metadata, write_content_attachment, - load_content_attachment, load_content, display_result, merge, for_each, and others. + load_content_attachment, load_content, display_result, merge, for_each, if_else, switch, and others. schema_version (str): Agent schema version. warnings (list[AgentDefinitionResponseWarningsType0Item] | None | Unset): Validation warnings, if any. """ diff --git a/seclai/_generated/models/agent_definition_response_definition.py b/seclai/_generated/models/agent_definition_response_definition.py index 4036221..3689015 100644 --- a/seclai/_generated/models/agent_definition_response_definition.py +++ b/seclai/_generated/models/agent_definition_response_definition.py @@ -15,7 +15,7 @@ class AgentDefinitionResponseDefinition: retrieval, regex_replace, gate, retry, evaluate_step, extract_data, extract_content, add_chat_turn, load_chat_history, add_memory, search_memory, load_memory, streaming_result, send_email, webhook_call, call_agent, write_metadata, write_content_attachment, load_content_attachment, load_content, display_result, merge, for_each, - and others. + if_else, switch, and others. """ diff --git a/seclai/_generated/models/agent_run_request.py b/seclai/_generated/models/agent_run_request.py index 429f67c..2c22f9a 100644 --- a/seclai/_generated/models/agent_run_request.py +++ b/seclai/_generated/models/agent_run_request.py @@ -22,16 +22,39 @@ class AgentRunRequest: Attributes: input_ (None | str | Unset): Input to provide to the agent upon running for agents with dynamic triggers. input_upload_id (None | Unset | UUID): ID of a previously uploaded file (via POST /{agent_id}/upload-input) to - use as the run input for dynamic-input triggers. Mutually exclusive with the 'input' field. + use as the run input for dynamic-input triggers. Mutually exclusive with the 'input' field. Use + ``input_upload_ids`` to attach multiple files. + + **Attachment visibility:** a step only sees the upload when its template references the input — via + ``{{input}}`` / ``{{agent.input}}`` / ``{{step..input|output}}`` (implicit, all attachments) or the + ``{{attachments[…]}}`` family (explicit narrowing — e.g. ``{{attachments[0]}}``, ``{{attachments[*.pdf]}}``). + + **Per-batch validation:** every selector the agent's definition declares must be satisfied or the run is + rejected with HTTP 400. Exact-name selectors require that filename to be present; indexed selectors require at + least N+1 files; glob patterns require at least one matching filename. + input_upload_ids (list[UUID] | None | Unset): IDs of multiple previously uploaded files. Each upload's extracted + text is concatenated under a heading; each upload's binary is surfaced as a separate ``MediaAttachment`` so + multi-modal prompt steps reason over all files at once. Steps narrow visibility via ``{{attachments[…]}}`` + selectors (by index, filename, or fnmatch glob). The batch must satisfy every selector the agent declares — + exact names, indexed references (length must exceed the highest index), and glob patterns (each pattern needs at + least one match). Mismatches return HTTP 400 with the unmet requirements listed. Mutually exclusive with + ``input`` and ``input_upload_id`` — pass exactly one of the three. Max 20 uploads per run. metadata (AgentRunRequestMetadataType0 | None | Unset): Metadata to make available for string substitution expressions in agent tasks. priority (bool | Unset): If true, the agent run will be treated as priority execution. Default: False. + replay_of_run_id (None | Unset | UUID): Re-run this agent reusing a prior run's uploaded input files. The files + are re-resolved server-side from the source run (which must belong to this account and agent) — you do not re- + upload them. Combine with ``input`` to change the text while keeping the files. A fresh upload batch + (``input_upload_id(s)``) takes precedence and disables replay. Binaries swept by retention fall back to their + extracted text. """ input_: None | str | Unset = UNSET input_upload_id: None | Unset | UUID = UNSET + input_upload_ids: list[UUID] | None | Unset = UNSET metadata: AgentRunRequestMetadataType0 | None | Unset = UNSET priority: bool | Unset = False + replay_of_run_id: None | Unset | UUID = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -53,6 +76,18 @@ def to_dict(self) -> dict[str, Any]: else: input_upload_id = self.input_upload_id + input_upload_ids: list[str] | None | Unset + if isinstance(self.input_upload_ids, Unset): + input_upload_ids = UNSET + elif isinstance(self.input_upload_ids, list): + input_upload_ids = [] + for input_upload_ids_type_0_item_data in self.input_upload_ids: + input_upload_ids_type_0_item = str(input_upload_ids_type_0_item_data) + input_upload_ids.append(input_upload_ids_type_0_item) + + else: + input_upload_ids = self.input_upload_ids + metadata: dict[str, Any] | None | Unset if isinstance(self.metadata, Unset): metadata = UNSET @@ -63,6 +98,14 @@ def to_dict(self) -> dict[str, Any]: priority = self.priority + replay_of_run_id: None | str | Unset + if isinstance(self.replay_of_run_id, Unset): + replay_of_run_id = UNSET + elif isinstance(self.replay_of_run_id, UUID): + replay_of_run_id = str(self.replay_of_run_id) + else: + replay_of_run_id = self.replay_of_run_id + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update({}) @@ -70,10 +113,14 @@ def to_dict(self) -> dict[str, Any]: field_dict["input"] = input_ if input_upload_id is not UNSET: field_dict["input_upload_id"] = input_upload_id + if input_upload_ids is not UNSET: + field_dict["input_upload_ids"] = input_upload_ids if metadata is not UNSET: field_dict["metadata"] = metadata if priority is not UNSET: field_dict["priority"] = priority + if replay_of_run_id is not UNSET: + field_dict["replay_of_run_id"] = replay_of_run_id return field_dict @@ -111,6 +158,30 @@ def _parse_input_upload_id(data: object) -> None | Unset | UUID: input_upload_id = _parse_input_upload_id(d.pop("input_upload_id", UNSET)) + def _parse_input_upload_ids(data: object) -> list[UUID] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + input_upload_ids_type_0 = [] + _input_upload_ids_type_0 = data + for input_upload_ids_type_0_item_data in _input_upload_ids_type_0: + input_upload_ids_type_0_item = UUID( + input_upload_ids_type_0_item_data + ) + + input_upload_ids_type_0.append(input_upload_ids_type_0_item) + + return input_upload_ids_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[UUID] | None | Unset, data) + + input_upload_ids = _parse_input_upload_ids(d.pop("input_upload_ids", UNSET)) + def _parse_metadata( data: object, ) -> AgentRunRequestMetadataType0 | None | Unset: @@ -132,11 +203,30 @@ def _parse_metadata( priority = d.pop("priority", UNSET) + def _parse_replay_of_run_id(data: object) -> None | Unset | UUID: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + replay_of_run_id_type_0 = UUID(data) + + return replay_of_run_id_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | Unset | UUID, data) + + replay_of_run_id = _parse_replay_of_run_id(d.pop("replay_of_run_id", UNSET)) + agent_run_request = cls( input_=input_, input_upload_id=input_upload_id, + input_upload_ids=input_upload_ids, metadata=metadata, priority=priority, + replay_of_run_id=replay_of_run_id, ) agent_run_request.additional_properties = d diff --git a/seclai/_generated/models/agent_run_response.py b/seclai/_generated/models/agent_run_response.py index 264c2c9..bef4fd7 100644 --- a/seclai/_generated/models/agent_run_response.py +++ b/seclai/_generated/models/agent_run_response.py @@ -39,8 +39,16 @@ class AgentRunResponse: governance_input_status (None | str | Unset): Result of the governance input evaluation: safe, blocked, skipped, or timed_out. governance_input_wait_ms (int | None | Unset): Milliseconds spent waiting for governance input evaluation. + hitl_wait_ms (int | None | Unset): Cumulative milliseconds the run was parked waiting for a human decision on a + human_in_the_loop step. Subtracted from active duration in run-detail and duration-stats responses. input_scan_status (None | str | Unset): Result of the prompt injection scan: safe, unsafe, skipped, timed_out, or error. + output_content_type (None | str | Unset): MIME type of `output` — mirrors the terminal step's + `output_content_type`. Consumers interpret `output` differently depending on this value: + `application/vnd.seclai.manifest+json` is a multi-asset manifest with shape `{text, attachments: [{storage_key, + mime, name, bytes}]}` — fetch each attachment via `GET /authenticated/storage-blobs/{storage_key}`. + `text/plain` / `text/*` are free-form text. `application/json` is a JSON document. Null on runs that produced + no terminal output or that pre-date this column. scan_wait_ms (int | None | Unset): Milliseconds spent waiting for prompt injection scan. steps (list[AgentRunStepResponse] | None | Unset): Step outputs and per-step timing/credits. Only included when requested. @@ -58,7 +66,9 @@ class AgentRunResponse: flagged_policies: list[GovernancePolicyRefResponse] | Unset = UNSET governance_input_status: None | str | Unset = UNSET governance_input_wait_ms: int | None | Unset = UNSET + hitl_wait_ms: int | None | Unset = UNSET input_scan_status: None | str | Unset = UNSET + output_content_type: None | str | Unset = UNSET scan_wait_ms: int | None | Unset = UNSET steps: list[AgentRunStepResponse] | None | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -112,12 +122,24 @@ def to_dict(self) -> dict[str, Any]: else: governance_input_wait_ms = self.governance_input_wait_ms + hitl_wait_ms: int | None | Unset + if isinstance(self.hitl_wait_ms, Unset): + hitl_wait_ms = UNSET + else: + hitl_wait_ms = self.hitl_wait_ms + input_scan_status: None | str | Unset if isinstance(self.input_scan_status, Unset): input_scan_status = UNSET else: input_scan_status = self.input_scan_status + output_content_type: None | str | Unset + if isinstance(self.output_content_type, Unset): + output_content_type = UNSET + else: + output_content_type = self.output_content_type + scan_wait_ms: int | None | Unset if isinstance(self.scan_wait_ms, Unset): scan_wait_ms = UNSET @@ -158,8 +180,12 @@ def to_dict(self) -> dict[str, Any]: field_dict["governance_input_status"] = governance_input_status if governance_input_wait_ms is not UNSET: field_dict["governance_input_wait_ms"] = governance_input_wait_ms + if hitl_wait_ms is not UNSET: + field_dict["hitl_wait_ms"] = hitl_wait_ms if input_scan_status is not UNSET: field_dict["input_scan_status"] = input_scan_status + if output_content_type is not UNSET: + field_dict["output_content_type"] = output_content_type if scan_wait_ms is not UNSET: field_dict["scan_wait_ms"] = scan_wait_ms if steps is not UNSET: @@ -254,6 +280,15 @@ def _parse_governance_input_wait_ms(data: object) -> int | None | Unset: d.pop("governance_input_wait_ms", UNSET) ) + def _parse_hitl_wait_ms(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + hitl_wait_ms = _parse_hitl_wait_ms(d.pop("hitl_wait_ms", UNSET)) + def _parse_input_scan_status(data: object) -> None | str | Unset: if data is None: return data @@ -263,6 +298,17 @@ def _parse_input_scan_status(data: object) -> None | str | Unset: input_scan_status = _parse_input_scan_status(d.pop("input_scan_status", UNSET)) + def _parse_output_content_type(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + output_content_type = _parse_output_content_type( + d.pop("output_content_type", UNSET) + ) + def _parse_scan_wait_ms(data: object) -> int | None | Unset: if data is None: return data @@ -309,7 +355,9 @@ def _parse_steps(data: object) -> list[AgentRunStepResponse] | None | Unset: flagged_policies=flagged_policies, governance_input_status=governance_input_status, governance_input_wait_ms=governance_input_wait_ms, + hitl_wait_ms=hitl_wait_ms, input_scan_status=input_scan_status, + output_content_type=output_content_type, scan_wait_ms=scan_wait_ms, steps=steps, ) diff --git a/seclai/_generated/models/agent_run_step_response.py b/seclai/_generated/models/agent_run_step_response.py index 504141d..cfc6b7e 100644 --- a/seclai/_generated/models/agent_run_step_response.py +++ b/seclai/_generated/models/agent_run_step_response.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Mapping -from typing import Any, TypeVar, cast +from typing import TYPE_CHECKING, Any, TypeVar, cast from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -9,6 +9,11 @@ from ..models.pending_processing_completed_failed_status import ( PendingProcessingCompletedFailedStatus, ) +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.agent_run_tool_call_response import AgentRunToolCallResponse + T = TypeVar("T", bound="AgentRunStepResponse") @@ -27,6 +32,8 @@ class AgentRunStepResponse: started_at (None | str): Timestamp when the step attempt started. status (PendingProcessingCompletedFailedStatus): step_type (str): Type of the agent step. + tool_calls (list[AgentRunToolCallResponse] | Unset): LLM tool calls made during this step (prompt_call steps + only), ordered by execution. Empty for steps that invoked no tools. """ agent_step_id: str @@ -39,6 +46,7 @@ class AgentRunStepResponse: started_at: None | str status: PendingProcessingCompletedFailedStatus step_type: str + tool_calls: list[AgentRunToolCallResponse] | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -68,6 +76,13 @@ def to_dict(self) -> dict[str, Any]: step_type = self.step_type + tool_calls: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.tool_calls, Unset): + tool_calls = [] + for tool_calls_item_data in self.tool_calls: + tool_calls_item = tool_calls_item_data.to_dict() + tool_calls.append(tool_calls_item) + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -84,11 +99,15 @@ def to_dict(self) -> dict[str, Any]: "step_type": step_type, } ) + if tool_calls is not UNSET: + field_dict["tool_calls"] = tool_calls return field_dict @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.agent_run_tool_call_response import AgentRunToolCallResponse + d = dict(src_dict) agent_step_id = d.pop("agent_step_id") @@ -140,6 +159,17 @@ def _parse_started_at(data: object) -> None | str: step_type = d.pop("step_type") + _tool_calls = d.pop("tool_calls", UNSET) + tool_calls: list[AgentRunToolCallResponse] | Unset = UNSET + if _tool_calls is not UNSET: + tool_calls = [] + for tool_calls_item_data in _tool_calls: + tool_calls_item = AgentRunToolCallResponse.from_dict( + tool_calls_item_data + ) + + tool_calls.append(tool_calls_item) + agent_run_step_response = cls( agent_step_id=agent_step_id, credits_used=credits_used, @@ -151,6 +181,7 @@ def _parse_started_at(data: object) -> None | str: started_at=started_at, status=status, step_type=step_type, + tool_calls=tool_calls, ) agent_run_step_response.additional_properties = d diff --git a/seclai/_generated/models/agent_run_stream_request.py b/seclai/_generated/models/agent_run_stream_request.py index b63b16f..c3f68b4 100644 --- a/seclai/_generated/models/agent_run_stream_request.py +++ b/seclai/_generated/models/agent_run_stream_request.py @@ -24,14 +24,23 @@ class AgentRunStreamRequest: Attributes: input_ (None | str | Unset): Input to provide to the agent upon running for agents with dynamic triggers. input_upload_id (None | Unset | UUID): ID of a previously uploaded file (via POST /{agent_id}/upload-input) to - use as the run input for dynamic-input triggers. Mutually exclusive with the 'input' field. + use as the run input for dynamic-input triggers. Mutually exclusive with the 'input' field. Use + ``input_upload_ids`` to attach multiple files. Subject to the same per-batch attachment-selector validation as + the non-streaming endpoint. + input_upload_ids (list[UUID] | None | Unset): IDs of multiple previously uploaded files. See the non-streaming + endpoint for full semantics, including per-batch selector validation (exact names, indexed references, and glob + patterns must all be satisfied or the run is rejected with HTTP 400). Max 20. metadata (AgentRunStreamRequestMetadataType0 | None | Unset): Metadata to make available for string substitution expressions in agent tasks. + replay_of_run_id (None | Unset | UUID): Re-run reusing a prior run's uploaded input files (re-resolved server- + side from the source run, which must belong to this account and agent). A fresh upload batch takes precedence. """ input_: None | str | Unset = UNSET input_upload_id: None | Unset | UUID = UNSET + input_upload_ids: list[UUID] | None | Unset = UNSET metadata: AgentRunStreamRequestMetadataType0 | None | Unset = UNSET + replay_of_run_id: None | Unset | UUID = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -53,6 +62,18 @@ def to_dict(self) -> dict[str, Any]: else: input_upload_id = self.input_upload_id + input_upload_ids: list[str] | None | Unset + if isinstance(self.input_upload_ids, Unset): + input_upload_ids = UNSET + elif isinstance(self.input_upload_ids, list): + input_upload_ids = [] + for input_upload_ids_type_0_item_data in self.input_upload_ids: + input_upload_ids_type_0_item = str(input_upload_ids_type_0_item_data) + input_upload_ids.append(input_upload_ids_type_0_item) + + else: + input_upload_ids = self.input_upload_ids + metadata: dict[str, Any] | None | Unset if isinstance(self.metadata, Unset): metadata = UNSET @@ -61,6 +82,14 @@ def to_dict(self) -> dict[str, Any]: else: metadata = self.metadata + replay_of_run_id: None | str | Unset + if isinstance(self.replay_of_run_id, Unset): + replay_of_run_id = UNSET + elif isinstance(self.replay_of_run_id, UUID): + replay_of_run_id = str(self.replay_of_run_id) + else: + replay_of_run_id = self.replay_of_run_id + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update({}) @@ -68,8 +97,12 @@ def to_dict(self) -> dict[str, Any]: field_dict["input"] = input_ if input_upload_id is not UNSET: field_dict["input_upload_id"] = input_upload_id + if input_upload_ids is not UNSET: + field_dict["input_upload_ids"] = input_upload_ids if metadata is not UNSET: field_dict["metadata"] = metadata + if replay_of_run_id is not UNSET: + field_dict["replay_of_run_id"] = replay_of_run_id return field_dict @@ -107,6 +140,30 @@ def _parse_input_upload_id(data: object) -> None | Unset | UUID: input_upload_id = _parse_input_upload_id(d.pop("input_upload_id", UNSET)) + def _parse_input_upload_ids(data: object) -> list[UUID] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + input_upload_ids_type_0 = [] + _input_upload_ids_type_0 = data + for input_upload_ids_type_0_item_data in _input_upload_ids_type_0: + input_upload_ids_type_0_item = UUID( + input_upload_ids_type_0_item_data + ) + + input_upload_ids_type_0.append(input_upload_ids_type_0_item) + + return input_upload_ids_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[UUID] | None | Unset, data) + + input_upload_ids = _parse_input_upload_ids(d.pop("input_upload_ids", UNSET)) + def _parse_metadata( data: object, ) -> AgentRunStreamRequestMetadataType0 | None | Unset: @@ -126,10 +183,29 @@ def _parse_metadata( metadata = _parse_metadata(d.pop("metadata", UNSET)) + def _parse_replay_of_run_id(data: object) -> None | Unset | UUID: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + replay_of_run_id_type_0 = UUID(data) + + return replay_of_run_id_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | Unset | UUID, data) + + replay_of_run_id = _parse_replay_of_run_id(d.pop("replay_of_run_id", UNSET)) + agent_run_stream_request = cls( input_=input_, input_upload_id=input_upload_id, + input_upload_ids=input_upload_ids, metadata=metadata, + replay_of_run_id=replay_of_run_id, ) agent_run_stream_request.additional_properties = d diff --git a/seclai/_generated/models/agent_run_tool_call_response.py b/seclai/_generated/models/agent_run_tool_call_response.py new file mode 100644 index 0000000..375f3fb --- /dev/null +++ b/seclai/_generated/models/agent_run_tool_call_response.py @@ -0,0 +1,228 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="AgentRunToolCallResponse") + + +@_attrs_define +class AgentRunToolCallResponse: + """A single LLM tool call made during a prompt_call step. + + Attributes: + function_name (str): Name of the tool/function invoked. + id (str): Tool call identifier. + credits_used (float | Unset): Credits consumed by this tool call (0 for tools that don't bill). Default: 0.0. + duration_seconds (float | None | Unset): Duration of the tool call in seconds. + ended_at (None | str | Unset): Timestamp when the tool call ended. + error (None | str | Unset): Error message when the tool call failed. + input_ (None | str | Unset): JSON arguments the LLM passed to the tool, if persisted. + output (None | str | Unset): JSON result the tool returned to the LLM, if persisted. + round_index (int | Unset): 0-based tool-loop round this call belonged to. Default: 0. + sequence (int | Unset): 0-based ordinal of this call within its step run. Default: 0. + started_at (None | str | Unset): Timestamp when the tool call started. + succeeded (bool | Unset): Whether the tool call completed without error. Default: True. + """ + + function_name: str + id: str + credits_used: float | Unset = 0.0 + duration_seconds: float | None | Unset = UNSET + ended_at: None | str | Unset = UNSET + error: None | str | Unset = UNSET + input_: None | str | Unset = UNSET + output: None | str | Unset = UNSET + round_index: int | Unset = 0 + sequence: int | Unset = 0 + started_at: None | str | Unset = UNSET + succeeded: bool | Unset = True + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + function_name = self.function_name + + id = self.id + + credits_used = self.credits_used + + duration_seconds: float | None | Unset + if isinstance(self.duration_seconds, Unset): + duration_seconds = UNSET + else: + duration_seconds = self.duration_seconds + + ended_at: None | str | Unset + if isinstance(self.ended_at, Unset): + ended_at = UNSET + else: + ended_at = self.ended_at + + error: None | str | Unset + if isinstance(self.error, Unset): + error = UNSET + else: + error = self.error + + input_: None | str | Unset + if isinstance(self.input_, Unset): + input_ = UNSET + else: + input_ = self.input_ + + output: None | str | Unset + if isinstance(self.output, Unset): + output = UNSET + else: + output = self.output + + round_index = self.round_index + + sequence = self.sequence + + started_at: None | str | Unset + if isinstance(self.started_at, Unset): + started_at = UNSET + else: + started_at = self.started_at + + succeeded = self.succeeded + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "function_name": function_name, + "id": id, + } + ) + if credits_used is not UNSET: + field_dict["credits_used"] = credits_used + if duration_seconds is not UNSET: + field_dict["duration_seconds"] = duration_seconds + if ended_at is not UNSET: + field_dict["ended_at"] = ended_at + if error is not UNSET: + field_dict["error"] = error + if input_ is not UNSET: + field_dict["input"] = input_ + if output is not UNSET: + field_dict["output"] = output + if round_index is not UNSET: + field_dict["round_index"] = round_index + if sequence is not UNSET: + field_dict["sequence"] = sequence + if started_at is not UNSET: + field_dict["started_at"] = started_at + if succeeded is not UNSET: + field_dict["succeeded"] = succeeded + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + function_name = d.pop("function_name") + + id = d.pop("id") + + credits_used = d.pop("credits_used", UNSET) + + def _parse_duration_seconds(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + duration_seconds = _parse_duration_seconds(d.pop("duration_seconds", UNSET)) + + def _parse_ended_at(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + ended_at = _parse_ended_at(d.pop("ended_at", UNSET)) + + def _parse_error(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + error = _parse_error(d.pop("error", UNSET)) + + def _parse_input_(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + input_ = _parse_input_(d.pop("input", UNSET)) + + def _parse_output(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + output = _parse_output(d.pop("output", UNSET)) + + round_index = d.pop("round_index", UNSET) + + sequence = d.pop("sequence", UNSET) + + def _parse_started_at(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + started_at = _parse_started_at(d.pop("started_at", UNSET)) + + succeeded = d.pop("succeeded", UNSET) + + agent_run_tool_call_response = cls( + function_name=function_name, + id=id, + credits_used=credits_used, + duration_seconds=duration_seconds, + ended_at=ended_at, + error=error, + input_=input_, + output=output, + round_index=round_index, + sequence=sequence, + started_at=started_at, + succeeded=succeeded, + ) + + agent_run_tool_call_response.additional_properties = d + return agent_run_tool_call_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/seclai/_generated/models/agent_summary_response.py b/seclai/_generated/models/agent_summary_response.py index d4be5d8..4a0f675 100644 --- a/seclai/_generated/models/agent_summary_response.py +++ b/seclai/_generated/models/agent_summary_response.py @@ -26,7 +26,6 @@ class AgentSummaryResponse: description (None | str): Agent description. id (str): Unique agent identifier. name (str): Agent name. - trigger_type (None | str): Trigger type for the agent. updated_at (str): ISO 8601 last-updated timestamp. default_evaluation_tier (None | str | Unset): Default evaluation tier: fast, balanced, or thorough. evaluation_mode (str | Unset): Evaluation mode: output_expectation, eval_and_retry, or sample_and_flag. Default: @@ -44,13 +43,13 @@ class AgentSummaryResponse: retry_on_failure (bool | Unset): Whether to retry on evaluation failure. Default: True. sampling_config (AgentSummaryResponseSamplingConfigType0 | None | Unset): Sampling configuration for sample_and_flag mode. + trigger_type (None | str | Unset): Trigger type for the agent. """ created_at: str description: None | str id: str name: str - trigger_type: None | str updated_at: str default_evaluation_tier: None | str | Unset = UNSET evaluation_mode: str | Unset = "eval_and_retry" @@ -61,6 +60,7 @@ class AgentSummaryResponse: prompt_model_auto_upgrade_strategy: str | Unset = "none" retry_on_failure: bool | Unset = True sampling_config: AgentSummaryResponseSamplingConfigType0 | None | Unset = UNSET + trigger_type: None | str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -77,9 +77,6 @@ def to_dict(self) -> dict[str, Any]: name = self.name - trigger_type: None | str - trigger_type = self.trigger_type - updated_at = self.updated_at default_evaluation_tier: None | str | Unset @@ -131,6 +128,12 @@ def to_dict(self) -> dict[str, Any]: else: sampling_config = self.sampling_config + trigger_type: None | str | Unset + if isinstance(self.trigger_type, Unset): + trigger_type = UNSET + else: + trigger_type = self.trigger_type + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -139,7 +142,6 @@ def to_dict(self) -> dict[str, Any]: "description": description, "id": id, "name": name, - "trigger_type": trigger_type, "updated_at": updated_at, } ) @@ -167,6 +169,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["retry_on_failure"] = retry_on_failure if sampling_config is not UNSET: field_dict["sampling_config"] = sampling_config + if trigger_type is not UNSET: + field_dict["trigger_type"] = trigger_type return field_dict @@ -191,13 +195,6 @@ def _parse_description(data: object) -> None | str: name = d.pop("name") - def _parse_trigger_type(data: object) -> None | str: - if data is None: - return data - return cast(None | str, data) - - trigger_type = _parse_trigger_type(d.pop("trigger_type")) - updated_at = d.pop("updated_at") def _parse_default_evaluation_tier(data: object) -> None | str | Unset: @@ -295,12 +292,20 @@ def _parse_sampling_config( sampling_config = _parse_sampling_config(d.pop("sampling_config", UNSET)) + def _parse_trigger_type(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + trigger_type = _parse_trigger_type(d.pop("trigger_type", UNSET)) + agent_summary_response = cls( created_at=created_at, description=description, id=id, name=name, - trigger_type=trigger_type, updated_at=updated_at, default_evaluation_tier=default_evaluation_tier, evaluation_mode=evaluation_mode, @@ -311,6 +316,7 @@ def _parse_sampling_config( prompt_model_auto_upgrade_strategy=prompt_model_auto_upgrade_strategy, retry_on_failure=retry_on_failure, sampling_config=sampling_config, + trigger_type=trigger_type, ) agent_summary_response.additional_properties = d diff --git a/seclai/_generated/models/ai_assistant_generate_request.py b/seclai/_generated/models/ai_assistant_generate_request.py index f692149..d506cd1 100644 --- a/seclai/_generated/models/ai_assistant_generate_request.py +++ b/seclai/_generated/models/ai_assistant_generate_request.py @@ -1,10 +1,14 @@ from __future__ import annotations +import datetime from collections.abc import Mapping -from typing import Any, TypeVar +from typing import Any, TypeVar, cast from attrs import define as _attrs_define from attrs import field as _attrs_field +from dateutil.parser import isoparse + +from ..types import UNSET, Unset T = TypeVar("T", bound="AiAssistantGenerateRequest") @@ -15,14 +19,26 @@ class AiAssistantGenerateRequest: Attributes: user_input (str): User input describing what to do + history_since (datetime.datetime | None | Unset): Optional ISO 8601 timestamp. When set, only conversation + turns created at or after this timestamp are loaded as context, scoping history to the current session so the + assistant remembers earlier turns in a create flow. """ user_input: str + history_since: datetime.datetime | None | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: user_input = self.user_input + history_since: None | str | Unset + if isinstance(self.history_since, Unset): + history_since = UNSET + elif isinstance(self.history_since, datetime.datetime): + history_since = self.history_since.isoformat() + else: + history_since = self.history_since + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -30,6 +46,8 @@ def to_dict(self) -> dict[str, Any]: "user_input": user_input, } ) + if history_since is not UNSET: + field_dict["history_since"] = history_since return field_dict @@ -38,8 +56,26 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: d = dict(src_dict) user_input = d.pop("user_input") + def _parse_history_since(data: object) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + history_since_type_0 = isoparse(data) + + return history_since_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + history_since = _parse_history_since(d.pop("history_since", UNSET)) + ai_assistant_generate_request = cls( user_input=user_input, + history_since=history_since, ) ai_assistant_generate_request.additional_properties = d diff --git a/seclai/_generated/models/attachment_refs_source_api_summary.py b/seclai/_generated/models/attachment_refs_source_api_summary.py new file mode 100644 index 0000000..270e504 --- /dev/null +++ b/seclai/_generated/models/attachment_refs_source_api_summary.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="AttachmentRefsSourceApiSummary") + + +@_attrs_define +class AttachmentRefsSourceApiSummary: + """Per-source attachment-reference summary. + + Attributes: + exact_names (list[str] | Unset): + indexes_max (int | None | Unset): + kinds (list[str] | Unset): + patterns (list[str] | Unset): + """ + + exact_names: list[str] | Unset = UNSET + indexes_max: int | None | Unset = UNSET + kinds: list[str] | Unset = UNSET + patterns: list[str] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + exact_names: list[str] | Unset = UNSET + if not isinstance(self.exact_names, Unset): + exact_names = self.exact_names + + indexes_max: int | None | Unset + if isinstance(self.indexes_max, Unset): + indexes_max = UNSET + else: + indexes_max = self.indexes_max + + kinds: list[str] | Unset = UNSET + if not isinstance(self.kinds, Unset): + kinds = self.kinds + + patterns: list[str] | Unset = UNSET + if not isinstance(self.patterns, Unset): + patterns = self.patterns + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if exact_names is not UNSET: + field_dict["exact_names"] = exact_names + if indexes_max is not UNSET: + field_dict["indexes_max"] = indexes_max + if kinds is not UNSET: + field_dict["kinds"] = kinds + if patterns is not UNSET: + field_dict["patterns"] = patterns + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + exact_names = cast(list[str], d.pop("exact_names", UNSET)) + + def _parse_indexes_max(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + indexes_max = _parse_indexes_max(d.pop("indexes_max", UNSET)) + + kinds = cast(list[str], d.pop("kinds", UNSET)) + + patterns = cast(list[str], d.pop("patterns", UNSET)) + + attachment_refs_source_api_summary = cls( + exact_names=exact_names, + indexes_max=indexes_max, + kinds=kinds, + patterns=patterns, + ) + + attachment_refs_source_api_summary.additional_properties = d + return attachment_refs_source_api_summary + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/seclai/_generated/models/import_skip_response.py b/seclai/_generated/models/import_skip_response.py index 5e91c2e..14b6230 100644 --- a/seclai/_generated/models/import_skip_response.py +++ b/seclai/_generated/models/import_skip_response.py @@ -21,8 +21,8 @@ class ImportSkipResponse: Used as the element type for ``import_warnings`` on every response model that accepts an ``agent_definition`` payload. - See :py:class:`services.agent_definition_import.AgentImportSkip` - for the full category list. + See ``services.agent_definition_import.AgentImportSkip`` for the + full category list. Lives here (not on each router) so the authenticated and public API responses share one definition — keeping the shape that diff --git a/seclai/_generated/models/insufficient_credits_detail.py b/seclai/_generated/models/insufficient_credits_detail.py new file mode 100644 index 0000000..ad64030 --- /dev/null +++ b/seclai/_generated/models/insufficient_credits_detail.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Literal, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="InsufficientCreditsDetail") + + +@_attrs_define +class InsufficientCreditsDetail: + """``detail`` body for a 402 ``insufficient_credits`` response. + + Attributes: + account_id (str): UUID of the account that ran out of credits. + error (Literal['insufficient_credits']): Stable machine-readable error code. + message (str): Human-readable explanation. + """ + + account_id: str + error: Literal["insufficient_credits"] + message: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + account_id = self.account_id + + error = self.error + + message = self.message + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "account_id": account_id, + "error": error, + "message": message, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + account_id = d.pop("account_id") + + error = cast(Literal["insufficient_credits"], d.pop("error")) + if error != "insufficient_credits": + raise ValueError( + f"error must match const 'insufficient_credits', got '{error}'" + ) + + message = d.pop("message") + + insufficient_credits_detail = cls( + account_id=account_id, + error=error, + message=message, + ) + + insufficient_credits_detail.additional_properties = d + return insufficient_credits_detail + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/seclai/_generated/models/insufficient_credits_response.py b/seclai/_generated/models/insufficient_credits_response.py new file mode 100644 index 0000000..da8b9cb --- /dev/null +++ b/seclai/_generated/models/insufficient_credits_response.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.insufficient_credits_detail import InsufficientCreditsDetail + + +T = TypeVar("T", bound="InsufficientCreditsResponse") + + +@_attrs_define +class InsufficientCreditsResponse: + """402 envelope returned when the account has exhausted its credits. + + Attributes: + detail (InsufficientCreditsDetail): ``detail`` body for a 402 ``insufficient_credits`` response. + """ + + detail: InsufficientCreditsDetail + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + detail = self.detail.to_dict() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "detail": detail, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.insufficient_credits_detail import InsufficientCreditsDetail + + d = dict(src_dict) + detail = InsufficientCreditsDetail.from_dict(d.pop("detail")) + + insufficient_credits_response = cls( + detail=detail, + ) + + insufficient_credits_response.additional_properties = d + return insufficient_credits_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/seclai/_generated/models/memory_bank_ai_assistant_request.py b/seclai/_generated/models/memory_bank_ai_assistant_request.py index adacf0b..a39b1b0 100644 --- a/seclai/_generated/models/memory_bank_ai_assistant_request.py +++ b/seclai/_generated/models/memory_bank_ai_assistant_request.py @@ -1,10 +1,12 @@ from __future__ import annotations +import datetime from collections.abc import Mapping from typing import TYPE_CHECKING, Any, TypeVar, cast from attrs import define as _attrs_define from attrs import field as _attrs_field +from dateutil.parser import isoparse from ..types import UNSET, Unset @@ -23,16 +25,18 @@ class MemoryBankAiAssistantRequest: Attributes: user_input (str): Natural-language description of the memory bank. - conversation_id (None | str | Unset): Previous conversation ID to continue. current_config (MemoryBankAiAssistantRequestCurrentConfigType0 | None | Unset): Current configuration to refine, if any. + history_since (datetime.datetime | None | Unset): Optional ISO 8601 timestamp. When set, only conversation + turns created at or after this timestamp are loaded as context, scoping history to the current session so the + assistant remembers earlier turns in a multi-turn refinement. """ user_input: str - conversation_id: None | str | Unset = UNSET current_config: MemoryBankAiAssistantRequestCurrentConfigType0 | None | Unset = ( UNSET ) + history_since: datetime.datetime | None | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -42,12 +46,6 @@ def to_dict(self) -> dict[str, Any]: user_input = self.user_input - conversation_id: None | str | Unset - if isinstance(self.conversation_id, Unset): - conversation_id = UNSET - else: - conversation_id = self.conversation_id - current_config: dict[str, Any] | None | Unset if isinstance(self.current_config, Unset): current_config = UNSET @@ -58,6 +56,14 @@ def to_dict(self) -> dict[str, Any]: else: current_config = self.current_config + history_since: None | str | Unset + if isinstance(self.history_since, Unset): + history_since = UNSET + elif isinstance(self.history_since, datetime.datetime): + history_since = self.history_since.isoformat() + else: + history_since = self.history_since + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -65,10 +71,10 @@ def to_dict(self) -> dict[str, Any]: "user_input": user_input, } ) - if conversation_id is not UNSET: - field_dict["conversation_id"] = conversation_id if current_config is not UNSET: field_dict["current_config"] = current_config + if history_since is not UNSET: + field_dict["history_since"] = history_since return field_dict @@ -81,15 +87,6 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: d = dict(src_dict) user_input = d.pop("user_input") - def _parse_conversation_id(data: object) -> None | str | Unset: - if data is None: - return data - if isinstance(data, Unset): - return data - return cast(None | str | Unset, data) - - conversation_id = _parse_conversation_id(d.pop("conversation_id", UNSET)) - def _parse_current_config( data: object, ) -> MemoryBankAiAssistantRequestCurrentConfigType0 | None | Unset: @@ -113,10 +110,27 @@ def _parse_current_config( current_config = _parse_current_config(d.pop("current_config", UNSET)) + def _parse_history_since(data: object) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + history_since_type_0 = isoparse(data) + + return history_since_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + history_since = _parse_history_since(d.pop("history_since", UNSET)) + memory_bank_ai_assistant_request = cls( user_input=user_input, - conversation_id=conversation_id, current_config=current_config, + history_since=history_since, ) memory_bank_ai_assistant_request.additional_properties = d diff --git a/seclai/_generated/models/modality_rate_response.py b/seclai/_generated/models/modality_rate_response.py new file mode 100644 index 0000000..a2737ed --- /dev/null +++ b/seclai/_generated/models/modality_rate_response.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="ModalityRateResponse") + + +@_attrs_define +class ModalityRateResponse: + """Per-modality rate for an LLM that prices image/audio/video output + (or input) at a rate distinct from the default text rate. + + Example: Gemini 3.1 Flash Image charges $3/1M output tokens for + text but $60/1M output tokens for generated images. The image rate + surfaces here with ``modality="image"`` and ``output_credits_per_1000_tokens`` + set; the default text rate stays on the parent model fields. + + Attributes: + modality (str): + input_credits_per_1000_tokens (float | None | Unset): + output_credits_per_1000_tokens (float | None | Unset): + """ + + modality: str + input_credits_per_1000_tokens: float | None | Unset = UNSET + output_credits_per_1000_tokens: float | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + modality = self.modality + + input_credits_per_1000_tokens: float | None | Unset + if isinstance(self.input_credits_per_1000_tokens, Unset): + input_credits_per_1000_tokens = UNSET + else: + input_credits_per_1000_tokens = self.input_credits_per_1000_tokens + + output_credits_per_1000_tokens: float | None | Unset + if isinstance(self.output_credits_per_1000_tokens, Unset): + output_credits_per_1000_tokens = UNSET + else: + output_credits_per_1000_tokens = self.output_credits_per_1000_tokens + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "modality": modality, + } + ) + if input_credits_per_1000_tokens is not UNSET: + field_dict["input_credits_per_1000_tokens"] = input_credits_per_1000_tokens + if output_credits_per_1000_tokens is not UNSET: + field_dict["output_credits_per_1000_tokens"] = ( + output_credits_per_1000_tokens + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + modality = d.pop("modality") + + def _parse_input_credits_per_1000_tokens(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + input_credits_per_1000_tokens = _parse_input_credits_per_1000_tokens( + d.pop("input_credits_per_1000_tokens", UNSET) + ) + + def _parse_output_credits_per_1000_tokens(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + output_credits_per_1000_tokens = _parse_output_credits_per_1000_tokens( + d.pop("output_credits_per_1000_tokens", UNSET) + ) + + modality_rate_response = cls( + modality=modality, + input_credits_per_1000_tokens=input_credits_per_1000_tokens, + output_credits_per_1000_tokens=output_credits_per_1000_tokens, + ) + + modality_rate_response.additional_properties = d + return modality_rate_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/seclai/_generated/models/pending_processing_completed_failed_status.py b/seclai/_generated/models/pending_processing_completed_failed_status.py index 06bd7ff..fa37fd4 100644 --- a/seclai/_generated/models/pending_processing_completed_failed_status.py +++ b/seclai/_generated/models/pending_processing_completed_failed_status.py @@ -6,6 +6,7 @@ class PendingProcessingCompletedFailedStatus(str, Enum): FAILED = "failed" PENDING = "pending" PROCESSING = "processing" + WAITING_HUMAN = "waiting_human" def __str__(self) -> str: return str(self.value) diff --git a/seclai/_generated/models/prompt_model_response.py b/seclai/_generated/models/prompt_model_response.py index 111d80f..9532c01 100644 --- a/seclai/_generated/models/prompt_model_response.py +++ b/seclai/_generated/models/prompt_model_response.py @@ -11,6 +11,7 @@ from ..types import UNSET, Unset if TYPE_CHECKING: + from ..models.modality_rate_response import ModalityRateResponse from ..models.prompt_model_response_payload_schema_type_0 import ( PromptModelResponsePayloadSchemaType0, ) @@ -50,6 +51,7 @@ class PromptModelResponse: prompt_call json_template payloads. payload_schema_source_url (None | str | Unset): Source URL used to derive payload_schema guidance for this model. + per_modality_rates (list[ModalityRateResponse] | Unset): released_at (datetime.datetime | None | Unset): schema_documentation_url (None | str | Unset): Model documentation URL with request/response payload details. schema_notes (None | str | Unset): Human-readable notes about request payload compatibility. @@ -57,6 +59,7 @@ class PromptModelResponse: sunset_at (datetime.datetime | None | Unset): supported_input_media (list[str] | None | Unset): supported_languages (list[str] | None | Unset): + supported_output_media (list[str] | None | Unset): supports_openai_arguments (bool | Unset): Default: False. supports_streaming (bool | Unset): Default: False. supports_structured_output (bool | Unset): Default: True. @@ -91,6 +94,7 @@ class PromptModelResponse: output_credits_per_1000_tokens: float | None | Unset = UNSET payload_schema: None | PromptModelResponsePayloadSchemaType0 | Unset = UNSET payload_schema_source_url: None | str | Unset = UNSET + per_modality_rates: list[ModalityRateResponse] | Unset = UNSET released_at: datetime.datetime | None | Unset = UNSET schema_documentation_url: None | str | Unset = UNSET schema_notes: None | str | Unset = UNSET @@ -98,6 +102,7 @@ class PromptModelResponse: sunset_at: datetime.datetime | None | Unset = UNSET supported_input_media: list[str] | None | Unset = UNSET supported_languages: list[str] | None | Unset = UNSET + supported_output_media: list[str] | None | Unset = UNSET supports_openai_arguments: bool | Unset = False supports_streaming: bool | Unset = False supports_structured_output: bool | Unset = True @@ -209,6 +214,13 @@ def to_dict(self) -> dict[str, Any]: else: payload_schema_source_url = self.payload_schema_source_url + per_modality_rates: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.per_modality_rates, Unset): + per_modality_rates = [] + for per_modality_rates_item_data in self.per_modality_rates: + per_modality_rates_item = per_modality_rates_item_data.to_dict() + per_modality_rates.append(per_modality_rates_item) + released_at: None | str | Unset if isinstance(self.released_at, Unset): released_at = UNSET @@ -261,6 +273,15 @@ def to_dict(self) -> dict[str, Any]: else: supported_languages = self.supported_languages + supported_output_media: list[str] | None | Unset + if isinstance(self.supported_output_media, Unset): + supported_output_media = UNSET + elif isinstance(self.supported_output_media, list): + supported_output_media = self.supported_output_media + + else: + supported_output_media = self.supported_output_media + supports_openai_arguments = self.supports_openai_arguments supports_streaming = self.supports_streaming @@ -359,6 +380,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["payload_schema"] = payload_schema if payload_schema_source_url is not UNSET: field_dict["payload_schema_source_url"] = payload_schema_source_url + if per_modality_rates is not UNSET: + field_dict["per_modality_rates"] = per_modality_rates if released_at is not UNSET: field_dict["released_at"] = released_at if schema_documentation_url is not UNSET: @@ -373,6 +396,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["supported_input_media"] = supported_input_media if supported_languages is not UNSET: field_dict["supported_languages"] = supported_languages + if supported_output_media is not UNSET: + field_dict["supported_output_media"] = supported_output_media if supports_openai_arguments is not UNSET: field_dict["supports_openai_arguments"] = supports_openai_arguments if supports_streaming is not UNSET: @@ -398,6 +423,7 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.modality_rate_response import ModalityRateResponse from ..models.prompt_model_response_payload_schema_type_0 import ( PromptModelResponsePayloadSchemaType0, ) @@ -563,6 +589,17 @@ def _parse_payload_schema_source_url(data: object) -> None | str | Unset: d.pop("payload_schema_source_url", UNSET) ) + _per_modality_rates = d.pop("per_modality_rates", UNSET) + per_modality_rates: list[ModalityRateResponse] | Unset = UNSET + if _per_modality_rates is not UNSET: + per_modality_rates = [] + for per_modality_rates_item_data in _per_modality_rates: + per_modality_rates_item = ModalityRateResponse.from_dict( + per_modality_rates_item_data + ) + + per_modality_rates.append(per_modality_rates_item) + def _parse_released_at(data: object) -> datetime.datetime | None | Unset: if data is None: return data @@ -666,6 +703,25 @@ def _parse_supported_languages(data: object) -> list[str] | None | Unset: d.pop("supported_languages", UNSET) ) + def _parse_supported_output_media(data: object) -> list[str] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + supported_output_media_type_0 = cast(list[str], data) + + return supported_output_media_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[str] | None | Unset, data) + + supported_output_media = _parse_supported_output_media( + d.pop("supported_output_media", UNSET) + ) + supports_openai_arguments = d.pop("supports_openai_arguments", UNSET) supports_streaming = d.pop("supports_streaming", UNSET) @@ -775,6 +831,7 @@ def _parse_variants( output_credits_per_1000_tokens=output_credits_per_1000_tokens, payload_schema=payload_schema, payload_schema_source_url=payload_schema_source_url, + per_modality_rates=per_modality_rates, released_at=released_at, schema_documentation_url=schema_documentation_url, schema_notes=schema_notes, @@ -782,6 +839,7 @@ def _parse_variants( sunset_at=sunset_at, supported_input_media=supported_input_media, supported_languages=supported_languages, + supported_output_media=supported_output_media, supports_openai_arguments=supports_openai_arguments, supports_streaming=supports_streaming, supports_structured_output=supports_structured_output, diff --git a/seclai/seclai.py b/seclai/seclai.py index 36e01bc..9fb6dd5 100644 --- a/seclai/seclai.py +++ b/seclai/seclai.py @@ -60,6 +60,9 @@ ) from seclai._generated.models.file_upload_response import FileUploadResponse from seclai._generated.models.http_validation_error import HTTPValidationError +from seclai._generated.models.insufficient_credits_response import ( + InsufficientCreditsResponse, +) from seclai._generated.models.source_list_response import SourceListResponse from seclai._generated.types import Response as OpenAPIResponse from seclai.auth import ( @@ -716,6 +719,16 @@ def run_agent(self, agent_id: str, body: AgentRunRequest) -> AgentRunResponse: response_text=None, validation_error=parsed, ) + if isinstance(parsed, InsufficientCreditsResponse): + # Defensive: a 402 is already raised by _raise_for_openapi_response + # above; this narrows the parsed union for the success path. + raise SeclaiAPIStatusError( + message="Insufficient credits", + status_code=int(response.status_code), + method="POST", + url=self._build_url(path), + response_text=None, + ) return parsed def run_streaming_agent_and_wait( @@ -1772,6 +1785,66 @@ def get_agent_input_upload_status( ), ) + def get_agent_attachment_references(self, agent_id: str) -> dict[str, Any]: + """Get the static attachment-reference contract for an agent. + + Call this before staging uploads to learn whether the agent accepts + files at all (``requires_uploads``) and which specific filenames, + indexes, or glob patterns its templates reference. A run-time upload + batch that does not satisfy every declared selector is rejected with + HTTP 400. + + Args: + agent_id: Agent identifier. + + Returns: + The agent's attachment-reference contract. + """ + return cast( + dict[str, Any], + self.request( + "GET", + f"/agents/{agent_id}/attachment-references", + ), + ) + + def download_agent_run_attachment( + self, + run_id: str, + attachment_id: str, + *, + download_name: str | None = None, + ) -> httpx.Response: + """Download an attachment emitted by a step in an agent run. + + Returns a **streaming** response. The caller is responsible for + consuming and closing the response:: + + response = client.download_agent_run_attachment("run", "att") + with response: + for chunk in response.iter_bytes(): + f.write(chunk) + + Args: + run_id: Run identifier. + attachment_id: URL-safe-base64-encoded ``storage_key`` of the + attachment (as surfaced in webhook/email payloads and run + output manifests). + download_name: Optional filename hint for the download disposition. + + Returns: + A streaming ``httpx.Response``. Must be closed by the caller. + """ + request = self._client.build_request( + "GET", + f"/v2/agent-runs/{run_id}/attachments/{attachment_id}", + params=_strip_none({"download_name": download_name}), + headers=_merge_request_headers(options=self._options, request_headers=None), + ) + response = self._client.send(request, stream=True) + _raise_for_status(response) + return response + # ── Agent AI Assistant ──────────────────────────────────────────────────── def generate_agent_steps( @@ -3495,6 +3568,17 @@ def cancel_experiment(self, experiment_id: str) -> JSONValue: "POST", f"/models/playground/experiments/{experiment_id}/cancel" ) + def delete_experiment(self, experiment_id: str) -> None: + """Soft-delete a model playground experiment. + + Removes the experiment from list/detail views while preserving audit + history. + + Args: + experiment_id: Experiment identifier. + """ + self.request("DELETE", f"/models/playground/experiments/{experiment_id}") + # ── Search ──────────────────────────────────────────────────────────────── def search( @@ -4061,6 +4145,16 @@ async def run_agent(self, agent_id: str, body: AgentRunRequest) -> AgentRunRespo response_text=None, validation_error=parsed, ) + if isinstance(parsed, InsufficientCreditsResponse): + # Defensive: a 402 is already raised by _raise_for_openapi_response + # above; this narrows the parsed union for the success path. + raise SeclaiAPIStatusError( + message="Insufficient credits", + status_code=int(response.status_code), + method="POST", + url=self._build_url(path), + response_text=None, + ) return parsed async def run_streaming_agent_and_wait( @@ -5111,6 +5205,69 @@ async def get_agent_input_upload_status( ), ) + async def get_agent_attachment_references(self, agent_id: str) -> dict[str, Any]: + """Get the static attachment-reference contract for an agent. + + Call this before staging uploads to learn whether the agent accepts + files at all (``requires_uploads``) and which specific filenames, + indexes, or glob patterns its templates reference. A run-time upload + batch that does not satisfy every declared selector is rejected with + HTTP 400. + + Args: + agent_id: Agent identifier. + + Returns: + The agent's attachment-reference contract. + """ + return cast( + dict[str, Any], + await self.request( + "GET", + f"/agents/{agent_id}/attachment-references", + ), + ) + + async def download_agent_run_attachment( + self, + run_id: str, + attachment_id: str, + *, + download_name: str | None = None, + ) -> httpx.Response: + """Download an attachment emitted by a step in an agent run. + + Returns a **streaming** response. The caller is responsible for + consuming and closing the response:: + + response = await client.download_agent_run_attachment("run", "att") + async with response: + async for chunk in response.aiter_bytes(): + f.write(chunk) + + Args: + run_id: Run identifier. + attachment_id: URL-safe-base64-encoded ``storage_key`` of the + attachment (as surfaced in webhook/email payloads and run + output manifests). + download_name: Optional filename hint for the download disposition. + + Returns: + A streaming ``httpx.Response``. Must be closed by the caller. + """ + headers = await _merge_request_headers_async( + options=self._options, request_headers=None + ) + request = self._client.build_request( + "GET", + f"/v2/agent-runs/{run_id}/attachments/{attachment_id}", + params=_strip_none({"download_name": download_name}), + headers=headers, + ) + response = await self._client.send(request, stream=True) + _raise_for_status(response) + return response + # ── Agent AI Assistant ──────────────────────────────────────────────────── async def generate_agent_steps( @@ -6863,6 +7020,17 @@ async def cancel_experiment(self, experiment_id: str) -> JSONValue: "POST", f"/models/playground/experiments/{experiment_id}/cancel" ) + async def delete_experiment(self, experiment_id: str) -> None: + """Soft-delete a model playground experiment. + + Removes the experiment from list/detail views while preserving audit + history. + + Args: + experiment_id: Experiment identifier. + """ + await self.request("DELETE", f"/models/playground/experiments/{experiment_id}") + # ── Search ──────────────────────────────────────────────────────────────── async def search( diff --git a/tests/test_new_methods.py b/tests/test_new_methods.py index be8b08f..76e44f9 100644 --- a/tests/test_new_methods.py +++ b/tests/test_new_methods.py @@ -256,6 +256,41 @@ def handler(req: httpx.Request) -> httpx.Response: client.get_agent_input_upload_status("a1", "u1") assert seen == {"method": "GET", "path": "/agents/a1/input-uploads/u1"} + def test_get_agent_attachment_references(self) -> None: + seen: dict[str, Any] = {} + + def handler(req: httpx.Request) -> httpx.Response: + seen["method"] = req.method + seen["path"] = req.url.path + return _json_response({"requires_uploads": False}) + + client = _sync_client(handler) + result = client.get_agent_attachment_references("a1") + assert seen == {"method": "GET", "path": "/agents/a1/attachment-references"} + assert result["requires_uploads"] is False + + def test_download_agent_run_attachment(self) -> None: + seen: dict[str, Any] = {} + + def handler(req: httpx.Request) -> httpx.Response: + seen["method"] = req.method + seen["path"] = req.url.path + seen["download_name"] = req.url.params.get("download_name") + return httpx.Response(status_code=200, content=b"file-bytes") + + client = _sync_client(handler) + resp = client.download_agent_run_attachment( + "r1", "att1", download_name="report.pdf" + ) + assert seen == { + "method": "GET", + "path": "/v2/agent-runs/r1/attachments/att1", + "download_name": "report.pdf", + } + resp.read() + assert resp.content == b"file-bytes" + resp.close() + # --------------------------------------------------------------------------- # Agent AI Assistant @@ -1405,6 +1440,21 @@ def handler(req: httpx.Request) -> httpx.Response: client.get_model_recommendations("m1") assert seen["path"] == "/models/m1/recommendations" + def test_delete_experiment(self) -> None: + seen: dict[str, Any] = {} + + def handler(req: httpx.Request) -> httpx.Response: + seen["method"] = req.method + seen["path"] = req.url.path + return httpx.Response(status_code=204) + + client = _sync_client(handler) + client.delete_experiment("exp1") + assert seen == { + "method": "DELETE", + "path": "/models/playground/experiments/exp1", + } + # --------------------------------------------------------------------------- # Search @@ -1618,6 +1668,55 @@ async def handler(req: httpx.Request) -> httpx.Response: await client.cancel_agent_run("r1") assert seen == {"method": "POST", "path": "/agents/runs/r1/cancel"} + @pytest.mark.asyncio + async def test_async_get_agent_attachment_references(self) -> None: + seen: dict[str, Any] = {} + + async def handler(req: httpx.Request) -> httpx.Response: + seen["method"] = req.method + seen["path"] = req.url.path + return _json_response({"requires_uploads": True}) + + client = _async_client(handler) + result = await client.get_agent_attachment_references("a1") + assert seen == {"method": "GET", "path": "/agents/a1/attachment-references"} + assert result["requires_uploads"] is True + + @pytest.mark.asyncio + async def test_async_download_agent_run_attachment(self) -> None: + seen: dict[str, Any] = {} + + async def handler(req: httpx.Request) -> httpx.Response: + seen["method"] = req.method + seen["path"] = req.url.path + return httpx.Response(status_code=200, content=b"file-bytes") + + client = _async_client(handler) + resp = await client.download_agent_run_attachment("r1", "att1") + assert seen == { + "method": "GET", + "path": "/v2/agent-runs/r1/attachments/att1", + } + await resp.aread() + assert resp.content == b"file-bytes" + await resp.aclose() + + @pytest.mark.asyncio + async def test_async_delete_experiment(self) -> None: + seen: dict[str, Any] = {} + + async def handler(req: httpx.Request) -> httpx.Response: + seen["method"] = req.method + seen["path"] = req.url.path + return httpx.Response(status_code=204) + + client = _async_client(handler) + await client.delete_experiment("exp1") + assert seen == { + "method": "DELETE", + "path": "/models/playground/experiments/exp1", + } + @pytest.mark.asyncio async def test_async_run_streaming_agent(self) -> None: async def handler(req: httpx.Request) -> httpx.Response: From 33e20fb753c05cda799b699a9b80a2ba77a2b811 Mon Sep 17 00:00:00 2001 From: Kim Burgaard Date: Thu, 4 Jun 2026 22:42:28 -0700 Subject: [PATCH 2/3] Addressed review comments --- poetry.lock | 6 ++-- pyproject.toml | 4 +++ seclai/seclai.py | 65 ++++++++++++++++++++++----------------- tests/test_new_methods.py | 18 +++++++++++ 4 files changed, 61 insertions(+), 32 deletions(-) diff --git a/poetry.lock b/poetry.lock index 22352b0..c98596a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -858,7 +858,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -966,7 +966,7 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -1032,4 +1032,4 @@ typing-extensions = ">=4.12.0" [metadata] lock-version = "2.1" python-versions = ">=3.11,<4.0" -content-hash = "9109c39e7824730d7072e4af4f88715b042ac3fff80bad5913f93dffe09d61a5" +content-hash = "5e71739f6027288347b06b1c86843785e221a3290b9ba4dfa29c7efbb5ebab31" diff --git a/pyproject.toml b/pyproject.toml index 6316e44..ef9d2f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,6 +101,10 @@ include = [ [tool.poetry.dependencies] pydantic = "^2.12.5" httpx = "^0.28.1" +# Required at runtime by the generated models: openapi-python-client emits +# `dateutil.parser.isoparse` for datetime fields, so this must be a runtime +# dependency (types-python-dateutil below only covers typing stubs). +python-dateutil = "^2.8.1" [tool.poetry.group.dev.dependencies] pytest = ">=8,<9" diff --git a/seclai/seclai.py b/seclai/seclai.py index 9fb6dd5..4d3ad84 100644 --- a/seclai/seclai.py +++ b/seclai/seclai.py @@ -60,9 +60,6 @@ ) from seclai._generated.models.file_upload_response import FileUploadResponse from seclai._generated.models.http_validation_error import HTTPValidationError -from seclai._generated.models.insufficient_credits_response import ( - InsufficientCreditsResponse, -) from seclai._generated.models.source_list_response import SourceListResponse from seclai._generated.types import Response as OpenAPIResponse from seclai.auth import ( @@ -719,17 +716,10 @@ def run_agent(self, agent_id: str, body: AgentRunRequest) -> AgentRunResponse: response_text=None, validation_error=parsed, ) - if isinstance(parsed, InsufficientCreditsResponse): - # Defensive: a 402 is already raised by _raise_for_openapi_response - # above; this narrows the parsed union for the success path. - raise SeclaiAPIStatusError( - message="Insufficient credits", - status_code=int(response.status_code), - method="POST", - url=self._build_url(path), - response_text=None, - ) - return parsed + # ``parsed`` can only be the success model here: _raise_for_openapi_response + # already raised for any non-200 status, including the 402 + # InsufficientCreditsResponse envelope. + return cast(AgentRunResponse, parsed) def run_streaming_agent_and_wait( self, @@ -1842,7 +1832,13 @@ def download_agent_run_attachment( headers=_merge_request_headers(options=self._options, request_headers=None), ) response = self._client.send(request, stream=True) - _raise_for_status(response) + if response.is_error: + # Read the body for a useful error message, then release the + # connection before raising — the caller never receives the + # streaming response on the error path. + response.read() + response.close() + _raise_for_status(response) return response # ── Agent AI Assistant ──────────────────────────────────────────────────── @@ -2700,7 +2696,13 @@ def download_source_export(self, source_id: str, export_id: str) -> httpx.Respon headers=_merge_request_headers(options=self._options, request_headers=None), ) response = self._client.send(request, stream=True) - _raise_for_status(response) + if response.is_error: + # Read the body for a useful error message, then release the + # connection before raising — the caller never receives the + # streaming response on the error path. + response.read() + response.close() + _raise_for_status(response) return response def estimate_source_export( @@ -4145,17 +4147,10 @@ async def run_agent(self, agent_id: str, body: AgentRunRequest) -> AgentRunRespo response_text=None, validation_error=parsed, ) - if isinstance(parsed, InsufficientCreditsResponse): - # Defensive: a 402 is already raised by _raise_for_openapi_response - # above; this narrows the parsed union for the success path. - raise SeclaiAPIStatusError( - message="Insufficient credits", - status_code=int(response.status_code), - method="POST", - url=self._build_url(path), - response_text=None, - ) - return parsed + # ``parsed`` can only be the success model here: _raise_for_openapi_response + # already raised for any non-200 status, including the 402 + # InsufficientCreditsResponse envelope. + return cast(AgentRunResponse, parsed) async def run_streaming_agent_and_wait( self, @@ -5265,7 +5260,13 @@ async def download_agent_run_attachment( headers=headers, ) response = await self._client.send(request, stream=True) - _raise_for_status(response) + if response.is_error: + # Read the body for a useful error message, then release the + # connection before raising — the caller never receives the + # streaming response on the error path. + await response.aread() + await response.aclose() + _raise_for_status(response) return response # ── Agent AI Assistant ──────────────────────────────────────────────────── @@ -6138,7 +6139,13 @@ async def download_source_export( headers=headers, ) response = await self._client.send(request, stream=True) - _raise_for_status(response) + if response.is_error: + # Read the body for a useful error message, then release the + # connection before raising — the caller never receives the + # streaming response on the error path. + await response.aread() + await response.aclose() + _raise_for_status(response) return response async def estimate_source_export( diff --git a/tests/test_new_methods.py b/tests/test_new_methods.py index 76e44f9..e36dd06 100644 --- a/tests/test_new_methods.py +++ b/tests/test_new_methods.py @@ -291,6 +291,24 @@ def handler(req: httpx.Request) -> httpx.Response: assert resp.content == b"file-bytes" resp.close() + def test_download_agent_run_attachment_error_closes_stream(self) -> None: + """On an error status the streaming response is closed before raising.""" + from seclai import SeclaiAPIStatusError + + captured: dict[str, httpx.Response] = {} + + def handler(req: httpx.Request) -> httpx.Response: + resp = httpx.Response(status_code=404, content=b"not found") + captured["resp"] = resp + return resp + + client = _sync_client(handler) + with pytest.raises(SeclaiAPIStatusError) as exc: + client.download_agent_run_attachment("r1", "missing") + # Body was read so the error carries detail, and the stream is released. + assert exc.value.status_code == 404 + assert captured["resp"].is_closed + # --------------------------------------------------------------------------- # Agent AI Assistant From 96fedaf571cdd6ba71c1fd5a38ce9b566a9c597d Mon Sep 17 00:00:00 2001 From: Kim Burgaard Date: Fri, 5 Jun 2026 00:06:37 -0700 Subject: [PATCH 3/3] Fixed attachment endpoint type --- openapi/seclai.openapi.json | 45 +++++++++++++++++-- ...ns_run_id_attachments_attachment_id_get.py | 26 ++++++----- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/openapi/seclai.openapi.json b/openapi/seclai.openapi.json index 53fa909..fdb4658 100644 --- a/openapi/seclai.openapi.json +++ b/openapi/seclai.openapi.json @@ -16937,11 +16937,50 @@ "responses": { "200": { "content": { - "application/json": { - "schema": {} + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + }, + "application/pdf": { + "schema": { + "format": "binary", + "type": "string" + } + }, + "application/vnd.seclai.manifest+json": { + "schema": { + "format": "binary", + "type": "string" + } + }, + "audio/*": { + "schema": { + "format": "binary", + "type": "string" + } + }, + "image/*": { + "schema": { + "format": "binary", + "type": "string" + } + }, + "text/plain": { + "schema": { + "format": "binary", + "type": "string" + } + }, + "video/*": { + "schema": { + "format": "binary", + "type": "string" + } } }, - "description": "Successful Response" + "description": "Attachment bytes streamed with the resolved MIME type (inline-safe types keep their declared type; everything else is ``application/octet-stream``)." }, "422": { "content": { diff --git a/seclai/_generated/api/agent_run_attachments/serve_agent_run_attachment_api_v2_agent_runs_run_id_attachments_attachment_id_get.py b/seclai/_generated/api/agent_run_attachments/serve_agent_run_attachment_api_v2_agent_runs_run_id_attachments_attachment_id_get.py index 565f8ef..aac5700 100644 --- a/seclai/_generated/api/agent_run_attachments/serve_agent_run_attachment_api_v2_agent_runs_run_id_attachments_attachment_id_get.py +++ b/seclai/_generated/api/agent_run_attachments/serve_agent_run_attachment_api_v2_agent_runs_run_id_attachments_attachment_id_get.py @@ -1,4 +1,5 @@ from http import HTTPStatus +from io import BytesIO from typing import Any from urllib.parse import quote from uuid import UUID @@ -8,7 +9,7 @@ from ... import errors from ...client import AuthenticatedClient, Client from ...models.http_validation_error import HTTPValidationError -from ...types import UNSET, Response, Unset +from ...types import UNSET, File, Response, Unset def _get_kwargs( @@ -48,9 +49,10 @@ def _get_kwargs( def _parse_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Any | HTTPValidationError | None: +) -> File | HTTPValidationError | None: if response.status_code == 200: - response_200 = response.json() + response_200 = File(payload=BytesIO(response.content)) + return response_200 if response.status_code == 422: @@ -66,7 +68,7 @@ def _parse_response( def _build_response( *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[Any | HTTPValidationError]: +) -> Response[File | HTTPValidationError]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -82,7 +84,7 @@ def sync_detailed( client: AuthenticatedClient | Client, download_name: None | str | Unset = UNSET, x_account_id: UUID | Unset = UNSET, -) -> Response[Any | HTTPValidationError]: +) -> Response[File | HTTPValidationError]: """Download an agent-run attachment Streams the bytes of an attachment emitted by a step in the given agent run. ``attachment_id`` is @@ -112,7 +114,7 @@ def sync_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Any | HTTPValidationError] + Response[File | HTTPValidationError] """ kwargs = _get_kwargs( @@ -136,7 +138,7 @@ def sync( client: AuthenticatedClient | Client, download_name: None | str | Unset = UNSET, x_account_id: UUID | Unset = UNSET, -) -> Any | HTTPValidationError | None: +) -> File | HTTPValidationError | None: """Download an agent-run attachment Streams the bytes of an attachment emitted by a step in the given agent run. ``attachment_id`` is @@ -166,7 +168,7 @@ def sync( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Any | HTTPValidationError + File | HTTPValidationError """ return sync_detailed( @@ -185,7 +187,7 @@ async def asyncio_detailed( client: AuthenticatedClient | Client, download_name: None | str | Unset = UNSET, x_account_id: UUID | Unset = UNSET, -) -> Response[Any | HTTPValidationError]: +) -> Response[File | HTTPValidationError]: """Download an agent-run attachment Streams the bytes of an attachment emitted by a step in the given agent run. ``attachment_id`` is @@ -215,7 +217,7 @@ async def asyncio_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Any | HTTPValidationError] + Response[File | HTTPValidationError] """ kwargs = _get_kwargs( @@ -237,7 +239,7 @@ async def asyncio( client: AuthenticatedClient | Client, download_name: None | str | Unset = UNSET, x_account_id: UUID | Unset = UNSET, -) -> Any | HTTPValidationError | None: +) -> File | HTTPValidationError | None: """Download an agent-run attachment Streams the bytes of an attachment emitted by a step in the given agent run. ``attachment_id`` is @@ -267,7 +269,7 @@ async def asyncio( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Any | HTTPValidationError + File | HTTPValidationError """ return (