diff --git a/aixplain/v1/factories/team_agent_factory/__init__.py b/aixplain/v1/factories/team_agent_factory/__init__.py index a65a2fa4..1f58e21e 100644 --- a/aixplain/v1/factories/team_agent_factory/__init__.py +++ b/aixplain/v1/factories/team_agent_factory/__init__.py @@ -26,7 +26,7 @@ from aixplain.enums.supplier import Supplier from aixplain.modules.agent import Agent -from aixplain.modules.team_agent import TeamAgent +from aixplain.modules.team_agent import TeamAgent, ContextOverflowStrategy from aixplain.utils import config from aixplain.factories.team_agent_factory.utils import build_team_agent from aixplain.utils.convert_datatype_utils import normalize_expected_output @@ -60,6 +60,7 @@ def create( instructions: Optional[Text] = None, output_format: Optional[OutputFormat] = None, expected_output: Optional[Union[BaseModel, Text, dict]] = None, + context_overflow_strategy: Optional[Union["ContextOverflowStrategy", Text]] = None, **kwargs, ) -> TeamAgent: """Create a new team agent in the platform. @@ -76,6 +77,7 @@ def create( instructions: The instructions to guide the team agent (i.e. appended in the prompt of the team agent). output_format: The output format to be used for the team agent. expected_output: The expected output to be used for the team agent. + context_overflow_strategy: Strategy for handling context window overflow. Defaults to None (disabled). **kwargs: Additional keyword arguments for backward compatibility (deprecated parameters). Returns: @@ -257,6 +259,12 @@ def _setup_llm_and_tool( if isinstance(output_format, OutputFormat): output_format = output_format.value payload["outputFormat"] = output_format + if context_overflow_strategy is not None: + from aixplain.modules.team_agent import ContextOverflowStrategy + + if isinstance(context_overflow_strategy, ContextOverflowStrategy): + context_overflow_strategy = context_overflow_strategy.value + payload["contextOverflowStrategy"] = context_overflow_strategy team_agent = build_team_agent(payload=internal_payload, agents=agent_list, api_key=api_key) team_agent.validate(raise_exception=True) diff --git a/aixplain/v1/factories/team_agent_factory/utils.py b/aixplain/v1/factories/team_agent_factory/utils.py index 66029b94..c02e3640 100644 --- a/aixplain/v1/factories/team_agent_factory/utils.py +++ b/aixplain/v1/factories/team_agent_factory/utils.py @@ -152,6 +152,7 @@ def get_cached_model(model_id: str) -> any: status=AssetStatus(payload["status"]), output_format=OutputFormat(payload.get("outputFormat", OutputFormat.TEXT)), expected_output=payload.get("expectedOutput", None), + context_overflow_strategy=payload.get("contextOverflowStrategy"), ) team_agent.url = urljoin(config.BACKEND_URL, f"sdk/agent-communities/{team_agent.id}/run") diff --git a/aixplain/v1/modules/model/rlm.py b/aixplain/v1/modules/model/rlm.py index aa95961a..f7262b29 100644 --- a/aixplain/v1/modules/model/rlm.py +++ b/aixplain/v1/modules/model/rlm.py @@ -57,25 +57,27 @@ The REPL environment is initialized with: 1. A `context` variable that contains extremely important information about your query. You should check the content of the `context` variable to understand what you are working with. Make sure you look through it sufficiently as you answer your query. -2. A `llm_query` function that allows you to query an LLM (that can handle around 500K chars) inside your REPL environment. +2. A `llm_query` function that allows you to query an LLM with a context window of {worker_context_window} inside your REPL environment. You must take this context window into consideration when deciding how much text to pass in each call. 3. The ability to use `print()` statements to view the output of your REPL code and continue your reasoning. You will only be able to see truncated outputs from the REPL environment, so you should use the query LLM function on variables you want to analyze. You will find this function especially useful when you have to analyze the semantics of the context. Use these variables as buffers to build up your final answer. Make sure to explicitly look through the entire context in REPL before answering your query. An example strategy is to first look at the context and figure out a chunking strategy, then break up the context into smart chunks, and query an LLM per chunk with a particular question and save the answers to a buffer, then query an LLM with all the buffers to produce your final answer. -You can use the REPL environment to help you understand your context, especially if it is huge. Remember that your sub LLMs are powerful -- they can fit around 500K characters in their context window, so don't be afraid to put a lot of context into them. +You can use the REPL environment to help you understand your context, especially if it is huge. Remember that your sub LLMs are powerful -- they have a context window of {worker_context_window}, so don't be afraid to put a lot of context into them. When you want to execute Python code in the REPL environment, wrap it in triple backticks with the 'repl' language identifier: ```repl # your Python code here chunk = context[:10000] -answer = llm_query(f"What is the key finding in this text?\\n{chunk}") +answer = llm_query(f"What is the key finding in this text?\\n{{chunk}}") print(answer) ``` IMPORTANT: When you are done with the iterative process, you MUST provide a final answer using one of these two forms (NOT inside a code block): -1. FINAL(your final answer here) — to provide the answer as literal text -2. FINAL_VAR(variable_name) — to return a variable you created in the REPL as your final answer +1. FINAL(your final answer here) — to provide the answer as literal text. Use `FINAL(...)` only when you are completely finished: you will make no further REPL calls, need no further inspection of REPL output, and are not including any REPL code in the same response. +2. FINAL_VAR(variable_name) — to return a variable you created in the REPL as your final answer. Use `FINAL_VAR(...)` only when that variable already contains your completed final answer and you will make no further REPL calls. + +Do not use `FINAL(...)` or `FINAL_VAR(...)` for intermediate status updates, plans, requests to inspect REPL output, statements such as needing more information, or any response that also includes REPL code to be executed first; those must be written as normal assistant text instead. Think step by step carefully, plan, and execute this plan immediately — do not just say what you will do. """ @@ -107,8 +109,8 @@ # Prompt Helpers -def _build_system_messages() -> List[Dict[str, str]]: - return [{"role": "system", "content": _SYSTEM_PROMPT}] +def _build_system_messages(worker_context_window: str) -> List[Dict[str, str]]: + return [{"role": "system", "content": _SYSTEM_PROMPT.format(worker_context_window=worker_context_window)}] def _next_action_message(query: str, iteration: int, force_final: bool = False) -> Dict[str, str]: @@ -238,6 +240,28 @@ def __init__( self._session_id: Optional[str] = None self._sandbox_tool: Optional[Model] = None self._messages: List[Dict[str, str]] = [] + self._used_credits: float = 0.0 + + # Worker Context Window + + def _get_worker_context_window(self) -> str: + """Return a human-readable description of the worker model's context window.""" + attributes = getattr(self.worker, "additional_info", {}).get("attributes", []) + raw = next( + (attr["code"] for attr in attributes if attr.get("name") == "max_context_length"), + None, + ) + if raw is not None: + try: + tokens = int(raw) + if tokens >= 1_000_000: + return f"{tokens / 1_000_000:.1f}M tokens" + if tokens >= 1_000: + return f"{tokens / 1_000:.0f}K tokens" + return f"{tokens} tokens" + except (ValueError, TypeError): + return str(raw) + return "a large context window" # Context Resolution @@ -422,7 +446,10 @@ def _setup_repl(self, context: Union[str, dict, list]) -> None: import time as __time import json as __json +_total_llm_query_credits = 0.0 + def llm_query(prompt): + global _total_llm_query_credits _headers = {{"x-api-key": "{self.api_key}", "Content-Type": "application/json"}} _payload = __json.dumps({{"data": prompt, "max_tokens": 8192}}) try: @@ -437,6 +464,7 @@ def llm_query(prompt): _r = __requests.get(_poll_url, headers=_headers, timeout=30) _result = _r.json() _wait = min(_wait * 1.1, 60) + _total_llm_query_credits += float(_result.get("usedCredits", 0) or 0) return str(_result.get("data", "Error: no data in worker response")) except Exception as _e: return f"Error: llm_query failed — {{_e}}" @@ -445,12 +473,14 @@ def llm_query(prompt): self._run_sandbox(llm_query_code) logging.debug("RLM: llm_query injected into sandbox.") - def _run_sandbox(self, code: str) -> None: - """Execute code in the sandbox, ignoring the output (used for setup steps).""" - self._sandbox_tool.run( + def _run_sandbox(self, code: str) -> ModelResponse: + """Execute code in the sandbox and return the raw response.""" + result = self._sandbox_tool.run( inputs={"code": code, "sessionId": self._session_id}, action="run", ) + self._used_credits += float(getattr(result, "used_credits", 0) or 0) + return result # Code Execution @@ -468,10 +498,7 @@ def _execute_code(self, code: str) -> str: Formatted string combining stdout and stderr. Returns "No output" if both are empty. """ - result = self._sandbox_tool.run( - inputs={"code": code, "sessionId": self._session_id}, - action="run", - ) + result = self._run_sandbox(code) stdout = result.data.get("stdout", "") if isinstance(result.data, dict) else "" stderr = result.data.get("stderr", "") if isinstance(result.data, dict) else "" @@ -497,10 +524,7 @@ def _get_repl_variable(self, variable_name: str) -> Optional[str]: String representation of the variable, or None if not found or on error. """ var = variable_name.strip().strip("\"'") - result = self._sandbox_tool.run( - inputs={"code": f"print(str({var}))", "sessionId": self._session_id}, - action="run", - ) + result = self._run_sandbox(f"print(str({var}))") stdout = result.data.get("stdout", "") if isinstance(result.data, dict) else "" stderr = result.data.get("stderr", "") if isinstance(result.data, dict) else "" @@ -509,6 +533,23 @@ def _get_repl_variable(self, variable_name: str) -> Optional[str]: return None return stdout.strip() if stdout else None + # Credit Tracking + + def _collect_llm_query_credits(self) -> None: + """Retrieve accumulated ``llm_query`` worker credits from the sandbox. + + The injected ``llm_query`` function tracks per-call ``usedCredits`` + from the worker model API in a global ``_total_llm_query_credits`` + variable inside the sandbox session. This method reads that variable + and adds it to ``self._used_credits``. + """ + try: + raw = self._get_repl_variable("_total_llm_query_credits") + if raw is not None: + self._used_credits += float(raw) + except Exception: + logging.debug("RLM: could not retrieve llm_query credits from sandbox.") + # Orchestrator def _orchestrator_completion(self, messages: List[Dict[str, str]]) -> str: @@ -532,6 +573,7 @@ def _orchestrator_completion(self, messages: List[Dict[str, str]]) -> str: # response = self.orchestrator.run(data={"messages": messages}) prompt = _messages_to_prompt(messages) response = self.orchestrator.run(data=prompt, max_tokens=8192) + self._used_credits += float(getattr(response, "used_credits", 0) or 0) if response.get("completed") or response["status"] == ResponseStatus.SUCCESS: return str(response["data"]) raise RuntimeError(f"Orchestrator model failed: {response.get('error_message', 'Unknown error')}") @@ -585,6 +627,9 @@ def run( - ``data``: The final answer string. - ``completed``: True on success. - ``run_time``: Total elapsed seconds. + - ``used_credits``: Total credits consumed across all + orchestrator calls, sandbox executions, and worker + ``llm_query()`` invocations. - ``iterations_used``: Number of orchestrator iterations (via ``response["iterations_used"]``). @@ -630,13 +675,14 @@ def run( iterations_used = 0 final_answer = None repl_logs: List[Dict] = [] + self._used_credits = 0.0 # Normalize context: resolve file paths and pathlib.Path objects context = self._resolve_context(context) # Initialize sandbox and conversation self._setup_repl(context) - self._messages = _build_system_messages() + self._messages = _build_system_messages(self._get_worker_context_window()) try: for iteration in range(self.max_iterations): @@ -692,14 +738,17 @@ def run( except Exception as e: error_msg = f"RLM run error: {str(e)}" logging.error(error_msg) + self._collect_llm_query_credits() return ModelResponse( status=ResponseStatus.FAILED, completed=True, error_message=error_msg, run_time=time.time() - start_time, + used_credits=self._used_credits, iterations_used=iterations_used, ) + self._collect_llm_query_credits() run_time = time.time() - start_time logging.info(f"RLM '{name}': done in {iterations_used} iterations, {run_time:.1f}s.") @@ -708,6 +757,7 @@ def run( data=final_answer or "", completed=True, run_time=run_time, + used_credits=self._used_credits, iterations_used=iterations_used, ) diff --git a/aixplain/v1/modules/team_agent/__init__.py b/aixplain/v1/modules/team_agent/__init__.py index 0b1cec20..de5a571c 100644 --- a/aixplain/v1/modules/team_agent/__init__.py +++ b/aixplain/v1/modules/team_agent/__init__.py @@ -57,6 +57,18 @@ from pydantic import BaseModel +class ContextOverflowStrategy(str, Enum): + """Strategy applied when input messages exceed the model's context window. + + Attributes: + TRUNCATE: Remove the oldest chat-history messages until the context fits. + SUMMARIZE: Replace the full chat history with an LLM-generated summary. + """ + + TRUNCATE = "truncate" + SUMMARIZE = "summarize" + + class InspectorTarget(str, Enum): """Target stages for inspector validation in the team agent pipeline. @@ -107,6 +119,8 @@ class TeamAgent(Model, DeployableMixin[Agent]): use_mentalist (bool): DEPRECATED. Whether to use Mentalist agent for pre-planning. """ + ContextOverflowStrategy = ContextOverflowStrategy + is_valid: bool def __init__( @@ -125,6 +139,7 @@ def __init__( instructions: Optional[Text] = None, output_format: OutputFormat = OutputFormat.TEXT, expected_output: Optional[Union[BaseModel, Text, dict]] = None, + context_overflow_strategy: Optional[Union[ContextOverflowStrategy, Text]] = None, **additional_info, ) -> None: """Initialize a TeamAgent instance. @@ -144,6 +159,8 @@ def __init__( instructions (Optional[Text], optional): Instructions for the team agent. Defaults to None. output_format (OutputFormat, optional): Output format. Defaults to OutputFormat.TEXT. expected_output (Optional[Union[BaseModel, Text, dict]], optional): Expected output format. Defaults to None. + context_overflow_strategy (Optional[Union[ContextOverflowStrategy, Text]], optional): + Strategy for handling context window overflow. Defaults to None (disabled). **additional_info: Additional keyword arguments. Deprecated Args: @@ -237,6 +254,9 @@ def __init__( self.is_valid = True self.output_format = output_format self.expected_output = expected_output + if isinstance(context_overflow_strategy, ContextOverflowStrategy): + context_overflow_strategy = context_overflow_strategy.value + self.context_overflow_strategy = context_overflow_strategy def generate_session_id(self, history: list = None) -> str: """Generate a new session ID for the team agent. @@ -580,6 +600,7 @@ def run( max_iterations: int = 30, trace_request: bool = False, progress_verbosity: Optional[str] = "compact", + context_overflow_strategy: Optional[Union[ContextOverflowStrategy, Text]] = None, **kwargs, ) -> AgentResponse: """Runs a team agent call. @@ -646,6 +667,7 @@ def run( output_format=output_format, expected_output=expected_output, trace_request=trace_request, + context_overflow_strategy=context_overflow_strategy, ) if response["status"] == ResponseStatus.FAILED: end = time.time() @@ -711,6 +733,7 @@ def run_async( expected_output: Optional[Union[BaseModel, Text, dict]] = None, evolve: Union[Dict[str, Any], EvolveParam, None] = None, trace_request: bool = False, + context_overflow_strategy: Optional[Union[ContextOverflowStrategy, Text]] = None, ) -> AgentResponse: """Runs asynchronously a Team Agent call. @@ -796,6 +819,11 @@ def run_async( if isinstance(output_format, OutputFormat): output_format = output_format.value + if context_overflow_strategy is None: + context_overflow_strategy = self.context_overflow_strategy + if isinstance(context_overflow_strategy, ContextOverflowStrategy): + context_overflow_strategy = context_overflow_strategy.value + payload = { "id": self.id, "query": input_data, @@ -806,6 +834,7 @@ def run_async( "maxIterations": (parameters["max_iterations"] if "max_iterations" in parameters else max_iterations), "outputFormat": output_format, "expectedOutput": expected_output, + "contextOverflowStrategy": context_overflow_strategy, }, "evolve": json.dumps(evolve_dict), } @@ -1016,6 +1045,7 @@ def to_dict(self) -> Dict: "instructions": self.instructions, "outputFormat": self.output_format.value, "expectedOutput": self.expected_output, + "contextOverflowStrategy": self.context_overflow_strategy, } @classmethod @@ -1095,6 +1125,7 @@ def from_dict(cls, data: Dict) -> "TeamAgent": instructions=data.get("instructions"), output_format=OutputFormat(data.get("outputFormat", OutputFormat.TEXT)), expected_output=data.get("expectedOutput"), + context_overflow_strategy=data.get("contextOverflowStrategy"), # Pass deprecated params via kwargs llm_id=data.get("llmId", "69b7e5f1b2fe44704ab0e7d0"), mentalist_llm=mentalist_llm, diff --git a/aixplain/v2/__init__.py b/aixplain/v2/__init__.py index 9da53df0..2f8217d5 100644 --- a/aixplain/v2/__init__.py +++ b/aixplain/v2/__init__.py @@ -3,7 +3,7 @@ from .core import Aixplain from .rlm import RLM, RLMResult from .utility import Utility -from .agent import Agent +from .agent import Agent, ContextOverflowStrategy from .tool import Tool from .actions import Input, Inputs, Action, Actions from .file import Resource @@ -18,6 +18,7 @@ EvaluatorType, EvaluatorConfig, EditorConfig, + PrebuiltInspector, ) from .meta_agents import Debugger, DebugResult from .agent_progress import AgentProgressTracker, ProgressFormat @@ -58,6 +59,7 @@ "RLMResult", "Utility", "Agent", + "ContextOverflowStrategy", "Tool", "Resource", "FileUploader", @@ -73,6 +75,7 @@ "EvaluatorType", "EvaluatorConfig", "EditorConfig", + "PrebuiltInspector", "ModelResponse", # Meta-agents "Debugger", diff --git a/aixplain/v2/agent.py b/aixplain/v2/agent.py index c20d8555..24c42bd7 100644 --- a/aixplain/v2/agent.py +++ b/aixplain/v2/agent.py @@ -112,6 +112,18 @@ class OutputFormat(str, Enum): JSON = "json" +class ContextOverflowStrategy(str, Enum): + """Strategy applied when input messages exceed the model's context window. + + Attributes: + TRUNCATE: Remove the oldest chat-history messages until the context fits. + SUMMARIZE: Replace the full chat history with an LLM-generated summary. + """ + + TRUNCATE = "truncate" + SUMMARIZE = "summarize" + + class AgentRunParams(BaseRunParams): """Parameters for running an agent. @@ -295,6 +307,7 @@ class Agent( RESPONSE_CLASS = AgentRunResult Task = Task OutputFormat = OutputFormat + ContextOverflowStrategy = ContextOverflowStrategy # Core fields from Swagger instructions: Optional[str] = None @@ -339,6 +352,10 @@ class Agent( resource_info: Optional[Dict[str, Any]] = field(default_factory=dict, metadata=config(field_name="resourceInfo")) max_iterations: Optional[int] = field(default=5, metadata=config(field_name="maxIterations")) max_tokens: Optional[int] = field(default=2048, metadata=config(field_name="maxTokens")) + context_overflow_strategy: Optional[str] = field( + default=None, + metadata=config(field_name="contextOverflowStrategy"), + ) # Internal state for progress tracking (excluded from serialization) _progress_tracker: Optional[Any] = field( @@ -372,6 +389,9 @@ def __post_init__(self) -> None: if isinstance(self.output_format, OutputFormat): self.output_format = self.output_format.value + if isinstance(self.context_overflow_strategy, ContextOverflowStrategy): + self.context_overflow_strategy = self.context_overflow_strategy.value + if isinstance(self.llm, Model): self.llm = self.llm.id @@ -491,6 +511,7 @@ def after_run( "max_iterations": "maxIterations", "max_time": "maxTime", "expected_output": "expectedOutput", + "context_overflow_strategy": "contextOverflowStrategy", } def run(self, *args: Any, **kwargs: Unpack[AgentRunParams]) -> AgentRunResult: @@ -858,21 +879,19 @@ def _normalize_parameter_for_api(param: dict) -> dict: def build_save_payload(self, **kwargs: Any) -> dict: """Build the payload for the save action.""" # Import Inspector from v2 module - from .inspector import Inspector + from .inspector import Inspector, PrebuiltInspector # Pre-serialize inspectors before to_dict() to avoid dataclass_json issues original_inspectors = self.inspectors if self.inspectors: serialized_inspectors = [] for inspector in self.inspectors: - if isinstance(inspector, Inspector): - # Use Inspector's to_dict method which handles callable policy serialization + if isinstance(inspector, (Inspector, PrebuiltInspector)): serialized_inspectors.append(inspector.to_dict()) elif isinstance(inspector, dict): - # Already serialized serialized_inspectors.append(inspector) else: - raise ValueError(f"Inspector must be Inspector instance or dict, got {type(inspector)}") + raise ValueError(f"Inspector must be Inspector, PrebuiltInspector, or dict, got {type(inspector)}") self.inspectors = serialized_inspectors # Pre-serialize inspector_targets to strings (enum values) @@ -964,6 +983,7 @@ def build_run_payload(self, **kwargs: Unpack[AgentRunParams]) -> dict: "maxTokens": getattr(self, "max_tokens", 2048), "maxIterations": getattr(self, "max_iterations", 5), "maxTime": 300, + "contextOverflowStrategy": getattr(self, "context_overflow_strategy", None), } for k, v in defaults.items(): diff --git a/aixplain/v2/agent_progress.py b/aixplain/v2/agent_progress.py index edfbf81b..2be1aefc 100644 --- a/aixplain/v2/agent_progress.py +++ b/aixplain/v2/agent_progress.py @@ -439,7 +439,7 @@ def _format_step_line( step_line += f" · ${step_credits:.6f}" token_text = self._format_token_usage_inline(step) if token_text: - step_line += f" · ⊘ {token_text}" + step_line += f" · {token_text}" step_line += f" · {agent_action_part}" return step_line @@ -595,7 +595,7 @@ def _print_token_usage(self, step: Dict) -> None: """Print token usage for a step if available.""" text = self._format_token_usage_inline(step) if text: - print(f" ⊘ Tokens: {text}") + print(f" Tokens: {text}") def _format_token_usage_inline(self, step: Dict) -> str: """Format token usage as a compact inline string, or empty if unavailable.""" @@ -608,12 +608,12 @@ def _format_token_usage_inline(self, step: Dict) -> str: parts = [] if input_tok is not None: - parts.append(f"in={input_tok}") + parts.append(f"↓{input_tok}") if output_tok is not None: - parts.append(f"out={output_tok}") + parts.append(f"↑{output_tok}") if total_tok is not None: - parts.append(f"total={total_tok}") - return " · ".join(parts) + parts.append(f"({total_tok})") + return " ".join(parts) def _print_completion_message(self, status: str, steps: List[Dict]) -> None: """Print final completion message with stats.""" @@ -625,8 +625,9 @@ def _print_completion_message(self, status: str, steps: List[Dict]) -> None: token_suffix = "" total_input = getattr(self, "_total_input_tokens", 0) total_output = getattr(self, "_total_output_tokens", 0) + total_tokens = total_input + total_output if total_input or total_output: - token_suffix = f" · ⊘ {total_input}→{total_output} tokens" + token_suffix = f" · ↓{total_input} ↑{total_output} ({total_tokens})" if status == "SUCCESS": print( diff --git a/aixplain/v2/inspector.py b/aixplain/v2/inspector.py index 12417839..4e7bbad9 100644 --- a/aixplain/v2/inspector.py +++ b/aixplain/v2/inspector.py @@ -235,6 +235,214 @@ def from_dict(cls, data: Dict[str, Any]) -> "Inspector": ) +PROMPT_INJECTION_GUARDRAIL_ASSET_ID = "69a9974367d506543103ca18" +PII_REDACTION_GUARDRAIL_ASSET_ID = "69cbf63cd74e334a6bacfeb1" + +_PREBUILT_REGISTRY: Dict[str, Dict[str, Any]] = { + "prompt_injection_guard": { + "name": "Prompt Injection Guard", + "category": "protection", + "description": "Detects prompt attacks before they influence planning or execution.", + "default_targets": [InspectorTarget.INPUT.value], + "default_action": {"type": InspectorAction.ABORT.value}, + "evaluator_asset_id": PROMPT_INJECTION_GUARDRAIL_ASSET_ID, + "supported_actions": [ + InspectorAction.CONTINUE.value, + InspectorAction.RERUN.value, + InspectorAction.ABORT.value, + ], + "vendor": "aws", + }, + "pii_redaction": { + "name": "PII Redaction", + "category": "redaction", + "description": "Finds sensitive information and returns redacted content from the guardrail evaluator.", + "default_targets": [InspectorTarget.INPUT.value], + "default_action": {"type": InspectorAction.EDIT.value}, + "evaluator_asset_id": PII_REDACTION_GUARDRAIL_ASSET_ID, + "supported_actions": [ + InspectorAction.CONTINUE.value, + InspectorAction.EDIT.value, + InspectorAction.ABORT.value, + ], + "vendor": "aws", + }, +} + + +@dataclass +class PrebuiltInspector: + """A lightweight preset reference that the backend resolves into a full Inspector. + + Instead of manually configuring an evaluator, action, and editor, users can + reference one of the platform's pre-built inspector presets by ID. The + backend's ``normalize_prebuilt_inspectors`` validator expands the reference + before the agent graph is constructed. + + Example:: + + from aixplain.v2 import PrebuiltInspector, InspectorTarget + + team = client.Agent( + name="Safe Agent", + agents=[agent1, agent2], + inspectors=[ + PrebuiltInspector.prompt_injection_guard(), + PrebuiltInspector.pii_redaction(targets=[InspectorTarget.OUTPUT]), + ], + ) + """ + + preset_id: str + targets: Optional[List[str]] = None + action: Optional[Dict[str, Any]] = None + severity: Optional[InspectorSeverity] = None + description: Optional[str] = None + name: Optional[str] = None + config: Optional[Dict[str, Any]] = None + + def __post_init__(self) -> None: + """Validate the inspector configuration after initialization.""" + if self.preset_id not in _PREBUILT_REGISTRY: + available = ", ".join(sorted(_PREBUILT_REGISTRY)) + raise ValueError(f"Unknown inspector preset '{self.preset_id}'. Available presets: {available}") + if self.targets is not None: + self.targets = [t.value if isinstance(t, InspectorTarget) else str(t).lower() for t in self.targets] + if self.action is not None: + action_type = self.action.get("type", "") + supported = _PREBUILT_REGISTRY[self.preset_id]["supported_actions"] + if str(action_type).lower() not in supported: + raise ValueError( + f"Action '{action_type}' is not supported by preset '{self.preset_id}'. " + f"Supported actions: {supported}" + ) + + def to_dict(self) -> Dict[str, Any]: + """Serialize to the lightweight reference format expected by the backend.""" + d: Dict[str, Any] = {"presetId": self.preset_id} + if self.name is not None: + d["name"] = self.name + if self.description is not None: + d["description"] = self.description + if self.targets is not None: + d["targets"] = list(self.targets) + if self.action is not None: + d["action"] = dict(self.action) + if self.severity is not None: + d["severity"] = self.severity.value + if self.config is not None: + d["config"] = dict(self.config) + return d + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "PrebuiltInspector": + """Create a PrebuiltInspector from a dictionary.""" + severity_raw = data.get("severity") + return cls( + preset_id=data["presetId"], + name=data.get("name"), + description=data.get("description"), + targets=data.get("targets"), + action=data.get("action"), + severity=InspectorSeverity(severity_raw) if severity_raw else None, + config=data.get("config"), + ) + + @staticmethod + def prompt_injection_guard( + *, + targets: Optional[List[Any]] = None, + action: Optional[Dict[str, Any]] = None, + severity: Optional[InspectorSeverity] = None, + name: Optional[str] = None, + description: Optional[str] = None, + ) -> "PrebuiltInspector": + """Create a Prompt Injection Guard inspector. + + Detects prompt injection attacks before they influence planning or + execution. Defaults to ``ABORT`` on ``INPUT``. + + Args: + targets: Override default targets (default: ``[InspectorTarget.INPUT]``). + action: Override default action dict (default: ``{"type": "abort"}``). + severity: Optional severity level. + name: Optional custom name for the inspector node. + description: Optional custom description. + + Returns: + A PrebuiltInspector configured for prompt injection detection. + """ + return PrebuiltInspector( + preset_id="prompt_injection_guard", + targets=targets, + action=action, + severity=severity, + name=name, + description=description, + ) + + @staticmethod + def pii_redaction( + *, + targets: Optional[List[Any]] = None, + action: Optional[Dict[str, Any]] = None, + severity: Optional[InspectorSeverity] = None, + name: Optional[str] = None, + description: Optional[str] = None, + ) -> "PrebuiltInspector": + """Create a PII Redaction inspector. + + Finds sensitive information (PII) and returns redacted content from the + guardrail evaluator. Defaults to ``EDIT`` on ``INPUT``. + + Args: + targets: Override default targets (default: ``[InspectorTarget.INPUT]``). + action: Override default action dict (default: ``{"type": "edit"}``). + severity: Optional severity level. + name: Optional custom name for the inspector node. + description: Optional custom description. + + Returns: + A PrebuiltInspector configured for PII redaction. + """ + return PrebuiltInspector( + preset_id="pii_redaction", + targets=targets, + action=action, + severity=severity, + name=name, + description=description, + ) + + @staticmethod + def list_presets() -> Dict[str, Dict[str, Any]]: + """Return metadata for all available pre-built inspector presets. + + Returns: + A dict mapping preset IDs to their metadata (name, category, + description, default targets/action, supported actions, vendor). + """ + return { + pid: { + "name": meta["name"], + "category": meta["category"], + "description": meta["description"], + "default_targets": list(meta["default_targets"]), + "default_action": dict(meta["default_action"]), + "supported_actions": list(meta["supported_actions"]), + "vendor": meta.get("vendor"), + } + for pid, meta in _PREBUILT_REGISTRY.items() + } + + +def is_prebuilt_inspector(obj: Any) -> bool: + """Return True if *obj* is a PrebuiltInspector or a dict preset reference.""" + if isinstance(obj, PrebuiltInspector): + return True + return isinstance(obj, dict) and isinstance(obj.get("presetId"), str) + + __all__ = [ "Inspector", "InspectorTarget", @@ -245,4 +453,6 @@ def from_dict(cls, data: Dict[str, Any]) -> "Inspector": "EvaluatorType", "EvaluatorConfig", "EditorConfig", + "PrebuiltInspector", + "is_prebuilt_inspector", ] diff --git a/aixplain/v2/rlm.py b/aixplain/v2/rlm.py index 2db27f08..b12c7ee2 100644 --- a/aixplain/v2/rlm.py +++ b/aixplain/v2/rlm.py @@ -46,25 +46,27 @@ The REPL environment is initialized with: 1. A `context` variable that contains extremely important information about your query. You should check the content of the `context` variable to understand what you are working with. Make sure you look through it sufficiently as you answer your query. -2. A `llm_query` function that allows you to query an LLM (that can handle around 500K chars) inside your REPL environment. +2. A `llm_query` function that allows you to query an LLM with a context window of {worker_context_window} inside your REPL environment. You must take this context window into consideration when deciding how much text to pass in each call. 3. The ability to use `print()` statements to view the output of your REPL code and continue your reasoning. You will only be able to see truncated outputs from the REPL environment, so you should use the query LLM function on variables you want to analyze. You will find this function especially useful when you have to analyze the semantics of the context. Use these variables as buffers to build up your final answer. Make sure to explicitly look through the entire context in REPL before answering your query. An example strategy is to first look at the context and figure out a chunking strategy, then break up the context into smart chunks, and query an LLM per chunk with a particular question and save the answers to a buffer, then query an LLM with all the buffers to produce your final answer. -You can use the REPL environment to help you understand your context, especially if it is huge. Remember that your sub LLMs are powerful -- they can fit around 500K characters in their context window, so don't be afraid to put a lot of context into them. +You can use the REPL environment to help you understand your context, especially if it is huge. Remember that your sub LLMs are powerful -- they have a context window of {worker_context_window}, so don't be afraid to put a lot of context into them. When you want to execute Python code in the REPL environment, wrap it in triple backticks with the 'repl' language identifier: ```repl # your Python code here chunk = context[:10000] -answer = llm_query(f"What is the key finding in this text?\\n{chunk}") +answer = llm_query(f"What is the key finding in this text?\\n{{chunk}}") print(answer) ``` IMPORTANT: When you are done with the iterative process, you MUST provide a final answer using one of these two forms (NOT inside a code block): -1. FINAL(your final answer here) — to provide the answer as literal text -2. FINAL_VAR(variable_name) — to return a variable you created in the REPL as your final answer +1. FINAL(your final answer here) — to provide the answer as literal text. Use `FINAL(...)` only when you are completely finished: you will make no further REPL calls, need no further inspection of REPL output, and are not including any REPL code in the same response. +2. FINAL_VAR(variable_name) — to return a variable you created in the REPL as your final answer. Use `FINAL_VAR(...)` only when that variable already contains your completed final answer and you will make no further REPL calls. + +Do not use `FINAL(...)` or `FINAL_VAR(...)` for intermediate status updates, plans, requests to inspect REPL output, statements such as needing more information, or any response that also includes REPL code to be executed first; those must be written as normal assistant text instead. Think step by step carefully, plan, and execute this plan immediately — do not just say what you will do. """ @@ -96,8 +98,8 @@ # Prompt Helpers -def _build_system_messages() -> List[Dict[str, str]]: - return [{"role": "system", "content": _SYSTEM_PROMPT}] +def _build_system_messages(worker_context_window: str) -> List[Dict[str, str]]: + return [{"role": "system", "content": _SYSTEM_PROMPT.format(worker_context_window=worker_context_window)}] def _next_action_message(query: str, iteration: int, force_final: bool = False) -> Dict[str, str]: @@ -151,11 +153,14 @@ class RLMResult(Result): Attributes: iterations_used: Number of orchestrator iterations consumed. + used_credits: Total credits consumed across all orchestrator calls, + sandbox executions, and worker ``llm_query()`` invocations. repl_logs: Per-iteration REPL execution log (excluded from serialization; present only on live instances). """ iterations_used: int = field(default=0) + used_credits: float = field(default=0.0, metadata=dj_config(field_name="usedCredits")) repl_logs: List[Dict] = field( default_factory=list, repr=False, @@ -257,6 +262,13 @@ class RLM(BaseResource, ToolableMixin): metadata=dj_config(exclude=lambda x: True), init=False, ) + _used_credits: float = field( + default=0.0, + repr=False, + compare=False, + metadata=dj_config(exclude=lambda x: True), + init=False, + ) def __post_init__(self) -> None: """Auto-assign a UUID when no id is provided.""" @@ -343,6 +355,25 @@ def _get_sandbox(self) -> Any: logger.debug("RLM: sandbox tool resolved.") return self._sandbox_tool + # Worker Context Window + + def _get_worker_context_window(self) -> str: + """Return a human-readable description of the worker model's context window.""" + worker = self._get_worker() + attrs = getattr(worker, "attributes", None) or {} + raw = attrs.get("max_context_length", None) + if raw is not None: + try: + tokens = int(raw) + if tokens >= 1_000_000: + return f"{tokens / 1_000_000:.1f}M tokens" + if tokens >= 1_000: + return f"{tokens / 1_000:.0f}K tokens" + return f"{tokens} tokens" + except (ValueError, TypeError): + return str(raw) + return "a large context window" + # Sandbox Setup def _setup_repl(self, context: Union[str, dict, list]) -> None: @@ -470,7 +501,10 @@ def _setup_repl(self, context: Union[str, dict, list]) -> None: import time as __time import json as __json +_total_llm_query_credits = 0.0 + def llm_query(prompt): + global _total_llm_query_credits _headers = {{"x-api-key": "{self.context.api_key}", "Content-Type": "application/json"}} _payload = __json.dumps({{"data": prompt, "max_tokens": 8192}}) try: @@ -485,6 +519,7 @@ def llm_query(prompt): _r = __requests.get(_poll_url, headers=_headers, timeout=30) _result = _r.json() _wait = min(_wait * 1.1, 60) + _total_llm_query_credits += float(_result.get("usedCredits", 0) or 0) return str(_result.get("data", "Error: no data in worker response")) except Exception as _e: return f"Error: llm_query failed \u2014 {{_e}}" @@ -496,10 +531,12 @@ def llm_query(prompt): def _run_sandbox(self, sandbox: Any, code: str) -> Any: """Execute code in the sandbox and return the raw ToolResult.""" - return sandbox.run( + result = sandbox.run( data={"code": code, "sessionId": self._session_id}, action="run", ) + self._used_credits += float(getattr(result, "used_credits", 0) or 0) + return result def _execute_code(self, code: str) -> str: """Execute a code block in the sandbox and return formatted output. @@ -551,6 +588,23 @@ def _get_repl_variable(self, variable_name: str) -> Optional[str]: return None return stdout.strip() if stdout else None + # Credit Tracking + + def _collect_llm_query_credits(self) -> None: + """Retrieve accumulated ``llm_query`` worker credits from the sandbox. + + The injected ``llm_query`` function tracks per-call ``usedCredits`` + from the worker model API in a global ``_total_llm_query_credits`` + variable inside the sandbox session. This method reads that variable + and adds it to ``self._used_credits``. + """ + try: + raw = self._get_repl_variable("_total_llm_query_credits") + if raw is not None: + self._used_credits += float(raw) + except Exception: + logger.debug("RLM: could not retrieve llm_query credits from sandbox.") + # Orchestrator def _orchestrator_completion(self, messages: List[Dict[str, str]]) -> str: @@ -569,6 +623,7 @@ def _orchestrator_completion(self, messages: List[Dict[str, str]]) -> str: ResourceError: If the orchestrator model call fails or returns an error. """ response = self._get_orchestrator().run(text=_messages_to_prompt(messages), max_tokens=8192) + self._used_credits += float(getattr(response, "used_credits", 0) or 0) if response.completed or response.status == "SUCCESS": return str(response.data) raise ResourceError( @@ -617,6 +672,8 @@ def run( - ``data``: Final answer string. - ``status``: ``"SUCCESS"`` or ``"FAILED"``. - ``completed``: ``True``. + - ``used_credits``: Total credits consumed across all orchestrator + calls, sandbox executions, and worker ``llm_query()`` invocations. - ``iterations_used``: Number of orchestrator iterations consumed. - ``repl_logs``: Per-iteration execution log (not serialized). @@ -655,11 +712,12 @@ def run( iterations_used = 0 final_answer: Optional[str] = None repl_logs: List[Dict] = [] + self._used_credits = 0.0 # Resolve file-path context, initialise sandbox + conversation context = self._resolve_context(context) self._setup_repl(context) - self._messages = _build_system_messages() + self._messages = _build_system_messages(self._get_worker_context_window()) try: for iteration in range(self.max_iterations): @@ -715,6 +773,7 @@ def run( except Exception as exc: error_msg = f"RLM run error: {exc}" logger.error(error_msg) + self._collect_llm_query_credits() result = RLMResult( status="FAILED", completed=True, @@ -722,9 +781,11 @@ def run( data=None, ) result.iterations_used = iterations_used + result.used_credits = self._used_credits result.repl_logs = repl_logs return result + self._collect_llm_query_credits() run_time = time.time() - start_time logger.info(f"RLM '{name}': done in {iterations_used} iteration(s), {run_time:.1f}s.") @@ -734,6 +795,7 @@ def run( data=final_answer or "", ) result.iterations_used = iterations_used + result.used_credits = self._used_credits result.repl_logs = repl_logs result._raw_data = {"run_time": run_time} return result diff --git a/docs/LLMS.md b/docs/LLMS.md new file mode 100644 index 00000000..abf8d1e1 --- /dev/null +++ b/docs/LLMS.md @@ -0,0 +1,84 @@ +# LLM-Friendly Documentation (`llms.txt` / `llms-full.txt`) + +This folder ships two machine-readable bundles of the aiXplain SDK v2 reference, following the [llmstxt.org](https://llmstxt.org) standard: + +| File | What it is | When to use | +| --- | --- | --- | +| [`llms.txt`](./llms.txt) | Curated index — title + one-line summary for every SDK v2 module, plus pointers to the full bundle | LLMs and humans browsing for the right module | +| [`llms-full.txt`](./llms-full.txt) | Single concatenated dump of every SDK v2 reference page | Drop the whole SDK reference into one context window | + +## Why these files matter + +- **One-shot context for AI coding agents.** Claude Code, Cursor, Codex, Copilot Chat, and any MCP-compatible agent can `fetch` `llms-full.txt` once and answer SDK questions without crawling the docs site. +- **Deterministic code suggestions.** Agents stop hallucinating method signatures because the entire v2 surface area is in their context. +- **Faster onboarding for new engineers.** Paste `llms-full.txt` into any chat assistant and get accurate "how do I…" answers grounded in the latest SDK. +- **Standards-compliant.** Follows the [llmstxt.org](https://llmstxt.org) convention so external tools (Mintlify, Cursor, Continue, etc.) discover them automatically. +- **Cheap to keep fresh.** Generated from the source markdown — no manual maintenance. + +## Regenerating + +Run from the repo root: + +```bash +python generate_llms_full.py +``` + +This rewrites both `docs/llms.txt` and `docs/llms-full.txt` from the SDK v2 sidebar (`docs/api-reference/python/api_sidebar.js`). + +## Keeping them in sync (engineers) + +Regenerate **every time you push SDK changes that touch `aixplain/v2/`** (new module, new method, signature change, docstring update). Two recommended ways: + +### Option A — GitHub Actions (recommended) + +Add this workflow at `.github/workflows/llms-docs.yml`: + +```yaml +name: Refresh LLM docs + +on: + push: + branches: [main] + paths: + - 'aixplain/v2/**' + - 'docs/api-reference/python/**' + - 'generate_llms_full.py' + +jobs: + regenerate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - run: python generate_llms_full.py + - name: Commit refreshed bundles + run: | + if [ -n "$(git status --porcelain docs/llms.txt docs/llms-full.txt)" ]; then + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add docs/llms.txt docs/llms-full.txt + git commit -m "docs: refresh llms.txt and llms-full.txt" + git push + fi +``` + +### Option B — Pre-push git hook + +Add to `.git/hooks/pre-push`: + +```bash +#!/usr/bin/env bash +python generate_llms_full.py +git add docs/llms.txt docs/llms-full.txt +git diff --cached --quiet || git commit -m "docs: refresh llms.txt and llms-full.txt" +``` + +```bash +chmod +x .git/hooks/pre-push +``` + +## How LLMs discover them + +Most agentic coding tools look for `/llms.txt` at the repo or domain root. Once these files land at `https://docs.aixplain.com/llms.txt` and `https://docs.aixplain.com/llms-full.txt`, no further configuration is needed — agents will auto-discover them. diff --git a/docs/api-reference/python/aixplain/v2/llms-full.txt b/docs/llms-full.txt similarity index 82% rename from docs/api-reference/python/aixplain/v2/llms-full.txt rename to docs/llms-full.txt index 582f7a5b..5d2078ed 100644 --- a/docs/api-reference/python/aixplain/v2/llms-full.txt +++ b/docs/llms-full.txt @@ -14,6 +14,570 @@ aiXplain SDK v2 - Modern Python SDK for the aiXplain platform. --- +## aixplain.v2.actions + +Source: `api-reference/python/aixplain/v2/actions` + +Unified Actions / Inputs hierarchy for models and tools. + +Object Hierarchy:: + + Actions — collection of Action objects + Action — metadata + owns its inputs + Inputs — collection of Input objects + Input — individual input with schema + current value + +Models have a single implicit "run" action. The ``model.inputs`` shorthand +skips the actions layer since there is nothing to disambiguate. + +Tools have multiple actions, so the full path is always used. + +### Input Objects + +```python +class Input() +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L25) + +Individual input with schema and current value. + +**Attributes**: + +- `name` - The input parameter name. +- `required` - Whether this input is required. +- `type` - The data type (e.g. ``"text"``, ``"number"``, ``"string"``). +- `value` - The current value (mutable). +- `required`0 - Human-readable description. + +#### __init__ + +```python +def __init__(name: str, + required: bool = False, + type: Optional[str] = None, + value: Any = None, + description: str = "", + *, + _validator: Optional[Callable[[Any], bool]] = None, + _metadata: Optional[Any] = None) -> None +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L36) + +Initialize an Input with schema metadata and an optional validator. + +#### name + +```python +@property +def name() -> str +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L57) + +The input parameter name. + +#### required + +```python +@property +def required() -> bool +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L62) + +Whether this input is required. + +#### type + +```python +@property +def type() -> Optional[str] +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L67) + +The data type string (e.g. ``"text"``, ``"number"``). + +#### value + +```python +@property +def value() -> Any +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L72) + +The current value. + +#### value + +```python +@value.setter +def value(val: Any) -> None +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L77) + +Set the value, running the validator if one was provided. + +#### description + +```python +@property +def description() -> str +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L87) + +Human-readable description of the input. + +#### reset + +```python +def reset(default: Any = None) -> None +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L91) + +Reset the value to the given default (bypasses validation). + +#### __eq__ + +```python +def __eq__(other: object) -> bool +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L95) + +Compare by value so ``inputs['temperature'] == 0.7`` works. + +#### __hash__ + +```python +def __hash__() -> int +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L101) + +Return an identity-based hash. + +#### __repr__ + +```python +def __repr__() -> str +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L105) + +Return a developer-friendly representation. + +#### from_parameter + +```python +@classmethod +def from_parameter(cls, param: "Parameter") -> "Input" +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L122) + +Build an ``Input`` from a model ``Parameter``. + +#### from_action_input_spec + +```python +@classmethod +def from_action_input_spec(cls, spec: "ActionInputSpec") -> "Input" +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L171) + +Build an ``Input`` from a tool ``ActionInputSpec``. + +### Inputs Objects + +```python +class Inputs() +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L215) + +Ordered collection of :class:`Input` objects. + +Supports dict-like access (``inputs["key"]``, ``inputs["key"] = val``) +and dot-notation (``inputs.key``, ``inputs.key = val``). + +Iterating, ``.keys()``, ``.values()``, and ``.items()`` operate on +*raw values* so that ``dict(inputs.items())`` gives a plain ``{name: value}`` +mapping suitable for API payloads. + +#### __init__ + +```python +def __init__(inputs: Optional[Dict[str, Input]] = None) -> None +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L226) + +Initialize from an optional ordered dict of :class:`Input` objects. + +#### __getitem__ + +```python +def __getitem__(key: str) -> Input +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L234) + +Return the :class:`Input` for *key*. + +#### __setitem__ + +```python +def __setitem__(key: str, value: Any) -> None +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L241) + +Set the *value* on the :class:`Input` identified by *key*. + +#### __contains__ + +```python +def __contains__(key: object) -> bool +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L249) + +Return whether *key* is a known input name. + +#### __len__ + +```python +def __len__() -> int +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L254) + +Return the number of inputs. + +#### __iter__ + +```python +def __iter__() +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L259) + +Iterate over input names. + +#### __getattr__ + +```python +def __getattr__(name: str) -> Input +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L268) + +Dot-notation read: ``inputs.temperature``. + +#### __setattr__ + +```python +def __setattr__(name: str, value: Any) -> None +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L275) + +Dot-notation write: ``inputs.temperature = 0.7``. + +#### keys + +```python +def keys() -> List[str] +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L290) + +Return input names. + +#### values + +```python +def values() -> List[Any] +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L295) + +Return raw values for all inputs. + +#### items + +```python +def items() -> List[tuple[str, Any]] +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L300) + +Return ``(name, value)`` pairs. + +#### get + +```python +def get(key: str, default: Any = None) -> Optional[Input] +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L305) + +Return the :class:`Input` for *key*, or *default*. + +#### update + +```python +def update(**kwargs: Any) -> None +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L312) + +Update multiple input values at once. + +#### reset + +```python +def reset(key: Optional[str] = None) -> None +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L317) + +Reset one or all inputs to their default values. + +#### copy + +```python +def copy() -> Dict[str, Any] +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L331) + +Return a shallow copy of ``{name: value}``. + +#### validate + +```python +def validate(data: Optional[Dict[str, Any]] = None) -> List[str] +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L335) + +Validate *data* (or current values) against input specs. + +Returns a list of error strings (empty means valid). + +#### required + +```python +@property +def required() -> List[str] +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L355) + +Names of all required inputs. + +#### __repr__ + +```python +def __repr__() -> str +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L364) + +Return ``Inputs({'key': value, ...})``. + +#### from_parameters + +```python +@classmethod +def from_parameters(cls, params: Optional[list]) -> "Inputs" +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L375) + +Build from a list of model ``Parameter`` objects. + +#### from_action_input_specs + +```python +@classmethod +def from_action_input_specs(cls, specs: Optional[list]) -> "Inputs" +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L386) + +Build from a list of tool ``ActionInputSpec`` objects. + +### Action Objects + +```python +class Action() +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L397) + +Metadata for a single action, owning its :class:`Inputs`. + +For models the single action is always ``"run"``. +For tools there may be many (e.g. ``"search_agents"``, ``"search_models"``). + +#### __init__ + +```python +def __init__(name: str, + description: Optional[str] = None, + *, + inputs: Optional[Inputs] = None, + _inputs_loader: Optional[Callable[[], Inputs]] = None) -> None +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L404) + +Initialize an Action with name, description and optional lazy inputs. + +#### name + +```python +@property +def name() -> str +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L420) + +The canonical action name. + +#### description + +```python +@property +def description() -> Optional[str] +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L425) + +Human-readable description of the action. + +#### inputs + +```python +@property +def inputs() -> Inputs +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L430) + +The action's :class:`Inputs` (lazily loaded for tool actions). + +#### __repr__ + +```python +def __repr__() -> str +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L440) + +Return a multi-line representation showing the action and its inputs. + +### Actions Objects + +```python +class Actions() +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L470) + +Ordered collection of :class:`Action` objects. + +For models this contains a single ``"run"`` action. +For tools it lazily discovers actions from the backend. + +#### __init__ + +```python +def __init__( + actions: Optional[Dict[str, Action]] = None, + *, + _action_factory: Optional[Callable[[str, Optional[str]], Action]] = None, + _actions_lister: Optional[Callable[[], List[tuple[str, + Optional[str]]]]] = None +) -> None +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L477) + +Initialize with either an eager dict or lazy factory/lister callbacks. + +#### __getitem__ + +```python +def __getitem__(key: str) -> Action +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L511) + +Return the :class:`Action` for *key* (case-insensitive). + +#### __contains__ + +```python +def __contains__(key: object) -> bool +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L532) + +Return whether *key* matches an available action name. + +#### __iter__ + +```python +def __iter__() +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L540) + +Iterate over available action names. + +#### __len__ + +```python +def __len__() -> int +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L545) + +Return the number of available actions. + +#### __repr__ + +```python +def __repr__() -> str +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L549) + +Return ``Actions(['action1', 'action2', ...])``. + +#### refresh + +```python +def refresh() -> None +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/actions.py#L555) + +Clear caches and force re-fetch on next access. + +--- + ## aixplain.v2.agent Source: `api-reference/python/aixplain/v2/agent` @@ -26,7 +590,7 @@ Agent module for aiXplain v2 SDK. class ConversationMessage(TypedDict) ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L35) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L37) Type definition for a conversation message in agent history. @@ -41,7 +605,7 @@ Type definition for a conversation message in agent history. def validate_history(history: List[Dict[str, Any]]) -> bool ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L47) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L49) Validates conversation history for agent sessions. @@ -77,7 +641,7 @@ with each message containing the required 'role' and 'content' fields and proper class OutputFormat(str, Enum) ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L105) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L107) Output format options for agent responses. @@ -87,25 +651,25 @@ Output format options for agent responses. class AgentRunParams(BaseRunParams) ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L113) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L115) Parameters for running an agent. **Attributes**: -- `sessionId` - Session ID for conversation continuity +- `session_id` - Session ID for conversation continuity - `query` - The query to run - `variables` - Variables to replace {{variable}} placeholders in instructions and description. The backend performs the actual substitution. -- `allowHistoryAndSessionId` - Allow both history and session ID +- `allow_history_and_session_id` - Allow both history and session ID - `tasks` - List of tasks for the agent - `prompt` - Custom prompt override - `history` - Conversation history -- `executionParams` - Execution parameters (maxTokens, etc.) +- `execution_params` - Execution parameters (maxTokens, etc.) - `criteria` - Criteria for evaluation - `evolve` - Evolution parameters - `query`0 - Inspector configurations -- `query`1 - Whether to run response generation. Defaults to True. +- `query`1 - Whether to run response generation. Defaults to False. - `query`2 - Display format - "status" (single line) or "logs" (timeline). If None (default), progress tracking is disabled. - `query`3 - Detail level - 1 (minimal), 2 (thoughts), 3 (full I/O) @@ -120,7 +684,7 @@ Parameters for running an agent. class AgentResponseData() ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L155) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L157) Data structure for agent response. @@ -133,7 +697,7 @@ Data structure for agent response. class AgentRunResult(Result) ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L168) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L170) Result from running an agent. @@ -141,6 +705,25 @@ Result from running an agent. Override type from base class +#### execution_id + +```python +@property +def execution_id() -> Optional[str] +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L189) + +Extract the execution ID from the poll URL or request_id. + +The execution ID can be used with ``Agent.poll()`` and +``Agent.sync_poll()`` to resume polling a previously started run +without persisting the full URL. + +**Returns**: + + The execution ID if available, None otherwise. + #### debug ```python @@ -149,7 +732,7 @@ def debug(prompt: Optional[str] = None, **kwargs: Any) -> "DebugResult" ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L186) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L209) Debug this agent response using the Debugger meta-agent. @@ -198,7 +781,7 @@ use the Debugger directly: aix.Debugger().debug_response(result) class Task() ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L239) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L262) A task definition for agent workflows. @@ -208,7 +791,7 @@ A task definition for agent workflows. def __post_init__() -> None ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L247) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L270) Initialize task dependencies after dataclass creation. @@ -225,7 +808,7 @@ class Agent(BaseResource, SearchResourceMixin[BaseSearchParams, "Agent"], RunnableResourceMixin[AgentRunParams, AgentRunResult]) ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L257) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L280) Agent resource class. @@ -235,7 +818,7 @@ Agent resource class. def __post_init__() -> None ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L320) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L352) Initialize agent after dataclass creation. @@ -245,7 +828,7 @@ Initialize agent after dataclass creation. def mark_as_deleted() -> None ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L361) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L402) Mark the agent as deleted by setting status to DELETED and calling parent method. @@ -256,7 +839,7 @@ def before_run(*args: Any, **kwargs: Unpack[AgentRunParams]) -> Optional[AgentRunResult] ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L368) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L409) Hook called before running the agent to validate and prepare state. @@ -267,7 +850,7 @@ def on_poll(response: AgentRunResult, **kwargs: Unpack[AgentRunParams]) -> None ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L409) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L450) Hook called after each poll to update progress display. @@ -283,7 +866,7 @@ def after_run(result: Union[AgentRunResult, Exception], *args: Any, **kwargs: Unpack[AgentRunParams]) -> Optional[AgentRunResult] ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L421) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L462) Hook called after running the agent for result transformation. @@ -293,7 +876,7 @@ Hook called after running the agent for result transformation. def run(*args: Any, **kwargs: Unpack[AgentRunParams]) -> AgentRunResult ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L440) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L496) Run the agent with optional progress display. @@ -318,7 +901,7 @@ Run the agent with optional progress display. def run_async(*args: Any, **kwargs: Unpack[AgentRunParams]) -> AgentRunResult ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L468) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L517) Run the agent asynchronously. @@ -331,7 +914,61 @@ Run the agent asynchronously. **Returns**: -- `AgentRunResult` - The result of the agent execution +- `AgentRunResult` - The result of the agent execution. Use ``result.url`` + to poll for completion via ``sync_poll(result.url)`` or + ``client.get(result.url)``. Do not construct + ``/sdk/runs/{execution_id}`` — that endpoint is not supported + for agent runs. + +#### poll + +```python +def poll(poll_url: str) -> AgentRunResult +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L558) + +Poll for the result of an asynchronous agent execution. + +Unlike the base implementation, *poll_url* may be either a full URL +(as returned in ``AgentRunResult.url``) **or** a bare execution ID. +When an execution ID is provided the correct +``/sdk/agents/{id}/result`` endpoint is used automatically, avoiding +the common mistake of calling the unsupported +``/sdk/runs/{id}`` endpoint. + +**Arguments**: + +- `poll_url` - Full poll URL or execution ID. + + +**Returns**: + + AgentRunResult with current execution status. + +#### sync_poll + +```python +def sync_poll(poll_url: str, + **kwargs: Unpack[AgentRunParams]) -> AgentRunResult +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L576) + +Poll until an asynchronous agent execution completes. + +Accepts either a full URL or a bare execution ID (see +:meth:`poll` for details). + +**Arguments**: + +- `poll_url` - Full poll URL or execution ID. +- `**kwargs` - Run parameters including ``timeout`` and ``wait_time``. + + +**Returns**: + + AgentRunResult with final execution status. #### save @@ -339,7 +976,7 @@ Run the agent asynchronously. def save(*args: Any, **kwargs: Any) -> "Agent" ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L519) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L618) Save the agent with dependency management. @@ -369,24 +1006,58 @@ child components before the agent itself is saved. def before_save(*args: Any, **kwargs: Any) -> Optional[dict] ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L635) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L734) Callback to be called before the resource is saved. Handles status transitions based on save type. -#### after_clone +#### after_duplicate + +```python +def after_duplicate(result: Union["Agent", Exception], + **kwargs: Any) -> Optional["Agent"] +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L749) + +Callback called after the agent is duplicated. + +Sets the duplicated agent's status to DRAFT. + +#### duplicate ```python -def after_clone(result: Union["Agent", Exception], - **kwargs: Any) -> Optional["Agent"] +@with_hooks +def duplicate(duplicate_subagents: bool = False, + name: Optional[str] = None) -> "Agent" ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L650) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L759) + +Duplicate this agent on the aiXplain platform (server-side). + +Creates a server-side copy of this agent with a clean usage baseline. +The duplicate inherits the original's ownership, team, and permissions +but resets all usage and cost metrics. + +**Arguments**: + +- `duplicate_subagents` - If True, recursively duplicates referenced subagents + so the duplicate has independent copies. If False, the duplicate + keeps references to the original subagents. Defaults to False. +- `name` - Custom name for the duplicate. If None, a unique name is + auto-generated by the platform. Defaults to None. + + +**Returns**: + +- `Agent` - The newly created duplicate agent. + -Callback called after the agent is cloned. +**Raises**: -Sets the cloned agent's status to DRAFT. +- `ResourceError` - If the duplication request fails. #### search @@ -397,7 +1068,7 @@ def search(cls: type["Agent"], **kwargs: Unpack[BaseSearchParams]) -> "Page[Agent]" ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L660) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L798) Search agents with optional query and filtering. @@ -417,7 +1088,7 @@ Search agents with optional query and filtering. def build_save_payload(**kwargs: Any) -> dict ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L680) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L858) Build the payload for the save action. @@ -427,7 +1098,7 @@ Build the payload for the save action. def build_run_payload(**kwargs: Unpack[AgentRunParams]) -> dict ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L777) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L953) Build the payload for the run action. @@ -438,7 +1109,7 @@ def generate_session_id( history: Optional[List[ConversationMessage]] = None) -> str ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L850) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent.py#L1030) Generate a unique session ID for agent conversations. @@ -571,7 +1242,7 @@ def start(format: ProgressFormat = ProgressFormat.STATUS, truncate: bool = True) -> None ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent_progress.py#L626) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent_progress.py#L659) Start progress tracking (call from before_run hook). @@ -587,7 +1258,7 @@ Start progress tracking (call from before_run hook). def update(response: Any) -> None ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent_progress.py#L764) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent_progress.py#L813) Update progress with poll response (call from on_poll hook). @@ -601,7 +1272,7 @@ Update progress with poll response (call from on_poll hook). def finish(response: Any) -> None ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent_progress.py#L792) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent_progress.py#L841) Finish progress tracking and print completion (call from after_run hook). @@ -618,7 +1289,7 @@ def stream_progress(url: str, truncate: bool = True) -> Any ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent_progress.py#L814) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/agent_progress.py#L863) Stream agent progress until completion (standalone polling mode). @@ -1270,7 +1941,7 @@ Core module for aiXplain v2 API. class Aixplain() ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/core.py#L30) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/core.py#L33) Main class for the Aixplain API. @@ -1286,13 +1957,13 @@ def __init__(api_key: Optional[str] = None, model_url: Optional[str] = None) -> None ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/core.py#L74) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/core.py#L78) Initialize the Aixplain class. **Arguments**: -- `api_key` _str, optional_ - The API key. Falls back to TEAM_API_KEY env var. +- `api_key` _str, optional_ - The API key. Falls back to TEAM_API_KEY or AIXPLAIN_API_KEY env var. - `backend_url` _str, optional_ - The backend URL. Falls back to BACKEND_URL env var. - `pipeline_url` _str, optional_ - The pipeline execution URL. Falls back to PIPELINES_RUN_URL env var. - `model_url` _str, optional_ - The model execution URL. Falls back to MODELS_RUN_URL env var. @@ -1303,7 +1974,7 @@ Initialize the Aixplain class. def init_client() -> None ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/core.py#L102) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/core.py#L113) Initialize the client. @@ -1313,7 +1984,7 @@ Initialize the client. def init_resources() -> None ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/core.py#L109) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/core.py#L120) Initialize the resources. @@ -2056,204 +2727,31 @@ Source: `api-reference/python/aixplain/v2/integration` Integration module for managing external service integrations. -### ActionInputsProxy Objects - -```python -class ActionInputsProxy() -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L17) - -Proxy object that provides both dict-like and dot notation access to action input parameters. - -This proxy dynamically fetches action input specifications from the container resource -when needed, allowing for runtime discovery and validation of action inputs. - -#### __init__ - -```python -def __init__(container, action_name: str) -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L24) - -Initialize ActionInputsProxy with container and action name. - -#### __getitem__ - -```python -def __getitem__(key: str) -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L93) - -Get input value by key. - -#### __setitem__ - -```python -def __setitem__(key: str, value) -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L97) - -Set input value by key. - -#### __contains__ - -```python -def __contains__(key: str) -> bool -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L101) - -Check if input parameter exists. - -#### __len__ - -```python -def __len__() -> int -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L109) - -Return the number of input parameters. - -#### __iter__ - -```python -def __iter__() -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L114) - -Iterate over input parameter keys. - -#### __getattr__ - -```python -def __getattr__(name: str) -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L120) - -Get input value by attribute name. - -#### __setattr__ - -```python -def __setattr__(name: str, value) -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L127) - -Set input value by attribute name. - -#### get - -```python -def get(key: str, default=None) -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L138) - -Get input value with optional default. - -#### update - -```python -def update(**kwargs) -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L145) - -Update multiple inputs at once. - -#### keys - -```python -def keys() -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L150) - -Get input parameter codes. - -#### values - -```python -def values() -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L155) - -Get input parameter values. - -#### items - -```python -def items() -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L160) - -Get input parameter code-value pairs. - -#### reset_input - -```python -def reset_input(input_code: str) -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L169) - -Reset an input parameter to its backend default value. - -#### reset_all_inputs - -```python -def reset_all_inputs() -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L175) - -Reset all input parameters to their backend default values. - -#### __repr__ - -```python -def __repr__() -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L180) - -Return string representation of the proxy. - -### Input Objects +### ActionInputSpec Objects ```python @dataclass_json @dataclass -class Input() +class ActionInputSpec() ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L188) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L20) -Input parameter for an action. +Backend input-parameter specification for an action (deserialization only). -### Action Objects +### ActionSpec Objects ```python @dataclass_json @dataclass(repr=False) -class Action() +class ActionSpec() ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L206) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L38) -Container for tool action information and inputs. +Backend action specification (deserialization only). #### __repr__ @@ -2261,28 +2759,9 @@ Container for tool action information and inputs. def __repr__() -> str ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L224) - -Return a string representation showing name and input parameters. - -#### get_inputs_proxy - -```python -def get_inputs_proxy(container) -> ActionInputsProxy -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L240) - -Get an ActionInputsProxy for this action from a container. - -**Arguments**: - -- `container` - The container resource (Tool or Integration) that can fetch action specs - - -**Returns**: +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L56) -- `ActionInputsProxy` - A proxy object for accessing action inputs +Return a concise representation of the action spec. ### ToolId Objects @@ -2293,7 +2772,7 @@ Get an ActionInputsProxy for this action from a container. class ToolId() ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L254) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L69) Result for tool operations. @@ -2306,206 +2785,108 @@ Result for tool operations. class IntegrationResult(Result) ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L263) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L78) Result for connection operations. -The backend returns the connection ID in data.id. - -### IntegrationSearchParams Objects - -```python -class IntegrationSearchParams(BaseSearchParams) -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L272) - -Parameters for listing integrations. - -### ActionMixin Objects - -```python -class ActionMixin() -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L278) - -Mixin class providing action-related functionality for integrations and tools. - -#### list_actions - -```python -def list_actions() -> List[Action] -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L284) - -List available actions for the integration. - -#### list_inputs - -```python -def list_inputs(*actions: str) -> List[Action] -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L311) - -List available inputs for the integration. - -#### actions - -```python -@cached_property -def actions() -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L336) - -Get a proxy object that provides access to actions with their inputs. - -This enables the syntax: mytool.actions['ACTION_NAME'].channel = 'value' - -**Returns**: - -- `ActionsProxy` - A proxy object for accessing actions and their inputs - -#### set_inputs - -```python -def set_inputs(inputs_dict: Dict[str, Dict[str, Any]]) -> None -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L346) - -Set multiple action inputs in bulk using a dictionary tree structure. - -This method allows you to set inputs for multiple actions at once. -Action names are automatically converted to lowercase for consistent lookup. - -**Arguments**: - -- `inputs_dict` - Dictionary in the format: - { -- `"ACTION_NAME"` - { -- `"input_param1"` - "value1", -- `"input_param2"` - "value2", - ... - }, -- `"ANOTHER_ACTION"` - { -- `"input_param1"` - "value1", - ... - } - } - - -**Example**: - - tool.set_inputs({ -- `'slack_send_message'` - { # Will work regardless of case -- `'channel'` - '`general`', -- `'text'` - 'Hello from bulk set!', -- `"ACTION_NAME"`0 - 'MyBot' - }, -- `"ACTION_NAME"`1 - { # Will also work -- `'channel'` - '`general`', -- `'text'` - 'Hello from bulk set!', -- `"ACTION_NAME"`0 - 'MyBot' - }, -- `"ACTION_NAME"`6 - { # Will also work -- `"ACTION_NAME"`7 - '`general`', -- `"ACTION_NAME"`9 - 'document.pdf' - } - }) - - -**Raises**: - -- `"input_param1"`0 - If an action name is not found or invalid -- `"input_param1"`1 - If an input parameter is not found for an action +The backend returns the connection ID in data.id. -### ActionsProxy Objects +### IntegrationSearchParams Objects ```python -class ActionsProxy() +class IntegrationSearchParams(BaseSearchParams) ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L412) - -Proxy object that provides access to actions with their inputs. +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L87) -This enables the syntax: mytool.actions['ACTION_NAME'].channel = 'value' +Parameters for listing integrations. -#### __init__ +### ActionMixin Objects ```python -def __init__(container) +@dataclass +class ActionMixin() ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L418) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L94) -Initialize ActionsProxy with container resource. +Mixin class providing action-related functionality for integrations and tools. -#### __getitem__ +#### list_actions ```python -def __getitem__(action_name: str) +def list_actions() -> List[ActionSpec] ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L446) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L116) -Get an action with its inputs proxy. +List available actions for the integration. + +**Returns**: -Converts action name to lowercase for consistent lookup. + List of :class:`ActionSpec` objects from the backend. -#### __getattr__ +#### list_inputs ```python -def __getattr__(attr_name: str) +def list_inputs(*actions: str) -> List[ActionSpec] ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L466) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L168) -Get an action with its inputs proxy using attribute notation. +List available inputs for the integration. -Converts attribute name to lowercase for consistent lookup. +.. deprecated:: + Use ``tool.actions['action_name'].inputs`` to discover and configure + action inputs instead. -#### __contains__ +#### actions ```python -def __contains__(action_name: str) -> bool +@cached_property +def actions() -> Actions ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L480) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L184) + +Collection of actions with their inputs. + +**Returns**: -Check if an action exists. + :class:`Actions` collection. Access individual actions via + ``tool.actions['ACTION_NAME']`` which returns an :class:`Action` + whose ``.inputs`` property lazily fetches input specs. -#### get_available_actions +#### set_inputs ```python -def get_available_actions() -> List[str] +def set_inputs(inputs_dict: Dict[str, Dict[str, Any]]) -> None ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L489) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L223) -Get a list of available action names. +Set multiple action inputs in bulk using a dictionary tree structure. -#### refresh_cache +**Arguments**: -```python -def refresh_cache() -``` +- `inputs_dict` - ``{"ACTION_NAME": {"input_param": "value", ...}, ...}`` + -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L494) +**Raises**: -Clear the actions cache to force re-fetching. +- `ValueError` - If an action name is not found or invalid. +- `KeyError` - If an input parameter is not found for an action. ### Integration Objects ```python +@dataclass_json + +@dataclass class Integration(Model, ActionMixin) ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L500) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L257) Resource for integrations. @@ -2518,7 +2899,7 @@ All connection logic is centralized here. def run(**kwargs: Any) -> IntegrationResult ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L526) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L282) Run the integration with validation. @@ -2528,7 +2909,7 @@ Run the integration with validation. def connect(**kwargs: Any) -> "Tool" ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L530) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L286) Connect the integration. @@ -2539,6 +2920,11 @@ that the user must visit to complete authentication before using the tool. - `Tool` - The created tool. If OAuth authentication is required, ``tool.redirect_url`` will contain the URL the user must visit. + + +**Raises**: + +- `ValueError` - If the connection fails (e.g., name already exists). #### handle_run_response @@ -2546,7 +2932,7 @@ that the user must visit to complete authentication before using the tool. def handle_run_response(response: dict, **kwargs: Any) -> IntegrationResult ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L549) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/integration.py#L313) Handle the response from the integration. @@ -2705,7 +3091,7 @@ def debug_response(response: "AgentRunResult", **kwargs: Any) -> DebugResult ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/meta_agents.py#L166) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/meta_agents.py#L167) Debug an agent response. @@ -2838,7 +3224,7 @@ Model resource for v2 API. class Message() ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L35) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L36) Message structure from the API response. @@ -2851,7 +3237,7 @@ Message structure from the API response. class Detail() ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L47) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L48) Detail structure from the API response. @@ -2864,10 +3250,13 @@ Detail structure from the API response. class Usage() ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L58) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L89) Usage structure from the API response. +Token counts are nullable because some model providers (GPT-5.4, Claude, +Mistral Large) return ``"NaN"`` or ``null`` instead of integers. + ### ModelResult Objects ```python @@ -2877,379 +3266,139 @@ Usage structure from the API response. class ModelResult(Result) ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L68) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L112) Result for model runs with specific fields from the backend response. ### StreamChunk Objects ```python -@dataclass -class StreamChunk() -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L78) - -A chunk of streamed response data. - -**Attributes**: - -- `status` - The current status of the streaming operation (IN_PROGRESS or SUCCESS) -- `data` - The content/token of this chunk -- `tool_calls` - Tool call deltas when stream uses OpenAI-style chunk format -- `usage` - Usage payload when provided in a stream chunk -- `finish_reason` - Completion reason for the current choice, when provided - -#### __post_init__ - -```python -def __post_init__() -> None -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L95) - -Ensure data remains a text chunk. - -### ModelResponseStreamer Objects - -```python -class ModelResponseStreamer(Iterator[StreamChunk]) -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L101) - -A streamer for model responses that yields chunks as they arrive. - -This class provides an iterator interface for streaming model responses. -It handles the conversion of Server-Sent Events (SSE) into StreamChunk objects -and manages the response status. - -The streamer can be used directly in a for loop or as a context manager -for proper resource cleanup. - -**Example**: - - >>> model = aix.Model.get("69b7e5f1b2fe44704ab0e7d0") # GPT-5.4 - >>> for chunk in model.run(text="Explain LLMs", stream=True): - ... print(chunk.data, end="", flush=True) - - >>> # With context manager for proper cleanup - >>> with model.run_stream(text="Hello") as stream: - ... for chunk in stream: - ... print(chunk.data, end="", flush=True) - -#### __init__ - -```python -def __init__(response: "requests.Response") -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L122) - -Initialize a new ModelResponseStreamer instance. - -**Arguments**: - -- `response` - A requests.Response object with streaming enabled - -#### __iter__ - -```python -def __iter__() -> Iterator[StreamChunk] -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L134) - -Return the iterator for the ModelResponseStreamer. - -#### __next__ - -```python -def __next__() -> StreamChunk -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L138) - -Return the next chunk of the response. - -**Returns**: - -- `StreamChunk` - A StreamChunk object containing the next chunk of the response. - - -**Raises**: - -- `StopIteration` - When the stream is complete - -#### close - -```python -def close() -> None -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L253) - -Close the underlying response connection. - -#### __enter__ - -```python -def __enter__() -> "ModelResponseStreamer" -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L258) - -Context manager entry. - -#### __exit__ - -```python -def __exit__(exc_type, exc_val, exc_tb) -> None -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L262) - -Context manager exit - ensures response is closed. - -### InputsProxy Objects - -```python -class InputsProxy() -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L267) - -Proxy object that provides both dict-like and dot notation access to model parameters. - -#### __init__ - -```python -def __init__(model) -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L270) - -Initialize InputsProxy with a model instance. - -#### __getitem__ - -```python -def __getitem__(key: str) -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L297) - -Dict-like access: inputs['temperature']. - -#### __setitem__ - -```python -def __setitem__(key: str, value) -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L303) - -Dict-like assignment: inputs['temperature'] = 0.7. - -#### __getattr__ - -```python -def __getattr__(name: str) -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L320) - -Dot notation access: inputs.temperature. - -#### __setattr__ - -```python -def __setattr__(name: str, value) -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L326) - -Dot notation assignment: inputs.temperature = 0.7. - -#### __contains__ - -```python -def __contains__(key: str) -> bool -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L345) - -Check if parameter exists: 'temperature' in inputs. - -#### __len__ - -```python -def __len__() -> int -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L349) - -Number of parameters. - -#### __iter__ - -```python -def __iter__() -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L353) - -Iterate over parameter names. - -#### keys - -```python -def keys() -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L357) - -Get parameter names. - -#### values - -```python -def values() -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L361) - -Get parameter values. - -#### items - -```python -def items() -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L365) - -Get parameter name-value pairs. - -#### get - -```python -def get(key: str, default=None) -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L369) - -Get parameter value with default. - -#### update - -```python -def update(**kwargs) +@dataclass +class StreamChunk() ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L375) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L123) + +A chunk of streamed response data. + +**Attributes**: -Update multiple parameters at once. +- `status` - The current status of the streaming operation (IN_PROGRESS or SUCCESS) +- `data` - The content/token of this chunk +- `tool_calls` - Tool call deltas when stream uses OpenAI-style chunk format +- `usage` - Usage payload when provided in a stream chunk +- `finish_reason` - Completion reason for the current choice, when provided -#### clear +#### __post_init__ ```python -def clear() +def __post_init__() -> None ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L383) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L140) -Reset all parameters to backend defaults. +Ensure data remains a text chunk. -#### copy +### ModelResponseStreamer Objects ```python -def copy() +class ModelResponseStreamer(Iterator[StreamChunk]) ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L388) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L146) -Get a copy of current parameter values. +A streamer for model responses that yields chunks as they arrive. -#### has_parameter +This class provides an iterator interface for streaming model responses. +It handles the conversion of Server-Sent Events (SSE) into StreamChunk objects +and manages the response status. -```python -def has_parameter(param_name: str) -> bool -``` +The streamer can be used directly in a for loop or as a context manager +for proper resource cleanup. -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L392) +**Example**: -Check if a parameter exists. + >>> model = aix.Model.get("69b7e5f1b2fe44704ab0e7d0") # GPT-5.4 + >>> for chunk in model.run(text="Explain LLMs", stream=True): + ... print(chunk.data, end="", flush=True) + + >>> # With context manager for proper cleanup + >>> with model.run_stream(text="Hello") as stream: + ... for chunk in stream: + ... print(chunk.data, end="", flush=True) -#### get_parameter_names +#### __init__ ```python -def get_parameter_names() -> list +def __init__(response: "requests.Response") ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L396) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L167) + +Initialize a new ModelResponseStreamer instance. + +**Arguments**: -Get a list of all available parameter names. +- `response` - A requests.Response object with streaming enabled -#### get_required_parameters +#### __iter__ ```python -def get_required_parameters() -> list +def __iter__() -> Iterator[StreamChunk] ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L400) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L179) -Get a list of required parameter names. +Return the iterator for the ModelResponseStreamer. -#### get_parameter_info +#### __next__ ```python -def get_parameter_info(param_name: str) +def __next__() -> StreamChunk ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L404) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L183) -Get information about a specific parameter. +Return the next chunk of the response. -#### get_all_parameters +**Returns**: -```python -def get_all_parameters() -> dict -``` +- `StreamChunk` - A StreamChunk object containing the next chunk of the response. + -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L410) +**Raises**: -Get all current parameter values. +- `StopIteration` - When the stream is complete -#### reset_parameter +#### close ```python -def reset_parameter(param_name: str) +def close() -> None ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L414) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L298) -Reset a parameter to its backend default value. +Close the underlying response connection. -#### reset_all_parameters +#### __enter__ ```python -def reset_all_parameters() +def __enter__() -> "ModelResponseStreamer" ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L425) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L303) -Reset all parameters to their backend default values. +Context manager entry. -#### __repr__ +#### __exit__ ```python -def __repr__() +def __exit__(exc_type, exc_val, exc_tb) -> None ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L470) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L307) -Return string representation of InputsProxy. +Context manager exit - ensures response is closed. #### find_supplier_by_id @@ -3257,7 +3406,7 @@ Return string representation of InputsProxy. def find_supplier_by_id(supplier_id: Union[str, int]) -> Optional[Supplier] ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L476) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L315) Find supplier enum by ID. @@ -3267,22 +3416,13 @@ Find supplier enum by ID. def find_function_by_id(function_id: str) -> Optional[Function] ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L485) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L324) Find function enum by ID. -### Attribute Objects - -```python -@dataclass_json - -@dataclass -class Attribute() -``` - -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L495) - -Common attribute structure from the API response. +Handles both SDK-style identifiers (``TEXT_GENERATION``) and the +kebab-case identifiers returned by the backend API +(``text-generation``). ### Parameter Objects @@ -3293,7 +3433,7 @@ Common attribute structure from the API response. class Parameter() ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L505) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L348) Common parameter structure from the API response. @@ -3306,7 +3446,7 @@ Common parameter structure from the API response. class Version() ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L521) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L364) Version structure from the API response. @@ -3319,7 +3459,7 @@ Version structure from the API response. class Pricing() ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L530) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L373) Pricing structure from the API response. @@ -3332,7 +3472,7 @@ Pricing structure from the API response. class VendorInfo() ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L540) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L383) Supplier information structure from the API response. @@ -3342,7 +3482,7 @@ Supplier information structure from the API response. class ModelSearchParams(BaseSearchParams) ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L548) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L391) Search parameters for model queries. @@ -3368,7 +3508,7 @@ Filter by path prefix (e.g., "openai/gpt-4") class ModelRunParams(BaseRunParams) ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L564) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L407) Parameters for running models. @@ -3388,7 +3528,7 @@ class Model(BaseResource, SearchResourceMixin[ModelSearchParams, "Model"], RunnableResourceMixin[ModelRunParams, ModelResult], ToolableMixin) ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L577) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L420) Resource for models. @@ -3398,10 +3538,31 @@ Resource for models. def __post_init__() ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L629) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L469) Initialize dynamic attributes based on backend parameters. +#### get_attribute + +```python +def get_attribute(key: str, default: Any = None) -> Any +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L476) + +Return an attribute value from the backend attribute map. + +#### actions + +```python +@property +def actions() -> Actions +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L483) + +Actions available on this model (always a single ``"run"`` action). + #### supports_tool_calling ```python @@ -3409,7 +3570,7 @@ Initialize dynamic attributes based on backend parameters. def supports_tool_calling() -> Optional[bool] ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L672) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L525) Return whether this LLM supports tool calling, inferred from backend params. @@ -3420,7 +3581,7 @@ Return whether this LLM supports tool calling, inferred from backend params. def supports_structured_output() -> Optional[bool] ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L687) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L540) Return whether this LLM supports structured output, inferred from backend params. @@ -3431,7 +3592,7 @@ Return whether this LLM supports structured output, inferred from backend params def is_sync_only() -> bool ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L702) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L555) Check if the model only supports synchronous execution. @@ -3446,7 +3607,7 @@ Check if the model only supports synchronous execution. def is_async_capable() -> bool ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L713) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L566) Check if the model supports asynchronous execution. @@ -3460,17 +3621,31 @@ Check if the model supports asynchronous execution. def __setattr__(name: str, value) ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L723) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L576) Handle bulk assignment to inputs. +#### build_run_payload + +```python +def build_run_payload(**kwargs: Unpack[ModelRunParams]) -> dict +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L585) + +Build the JSON payload for a model execution request. + +Strips SDK-only orchestration params (``timeout``, ``wait_time``, +``show_progress``, ``stream``) so they are never forwarded to the +backend API. + #### build_run_url ```python def build_run_url(**kwargs: Unpack[ModelRunParams]) -> str ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L732) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L595) Build the URL for running the model. @@ -3480,7 +3655,7 @@ Build the URL for running the model. def mark_as_deleted() -> None ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L737) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L600) Mark the model as deleted by setting status to DELETED and calling parent method. @@ -3492,7 +3667,7 @@ def get(cls: type["Model"], id: str, **kwargs: Unpack[BaseGetParams]) -> "Model" ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L745) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L608) Get a model by ID. @@ -3505,7 +3680,7 @@ def search(cls: type["Model"], **kwargs: Unpack[ModelSearchParams]) -> Page["Model"] ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L754) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L617) Search with optional query and filtering. @@ -3525,7 +3700,7 @@ Search with optional query and filtering. def run(**kwargs: Unpack[ModelRunParams]) -> ModelResult ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L775) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L638) Run the model with dynamic parameter validation and default handling. @@ -3539,7 +3714,7 @@ This method routes the execution based on the model's connection type: def run_async(**kwargs: Unpack[ModelRunParams]) -> ModelResult ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L816) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L680) Run the model asynchronously. @@ -3558,7 +3733,7 @@ This method routes the execution based on the model's connection type: def run_stream(**kwargs: Unpack[ModelRunParams]) -> ModelResponseStreamer ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L912) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L776) Run the model with streaming response. @@ -3581,6 +3756,7 @@ or processing large responses incrementally. - `ValidationError` - If the model explicitly does not support streaming (supports_streaming is False) +- `ValueError` - If required parameters are missing or have invalid types **Example**: @@ -3600,7 +3776,7 @@ or processing large responses incrementally. def as_tool() -> ToolDict ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L1044) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L909) Serialize this model as a tool for agent creation. @@ -3619,7 +3795,7 @@ expects for model tools. - function: The model's function type - type: Always "model" - version: The model's version - - assetId: The model's ID (same as id) + - asset_id: The model's ID (same as id) **Example**: @@ -3633,7 +3809,7 @@ expects for model tools. def get_parameters() -> List[dict] ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L1102) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/model.py#L967) Get current parameter values for this model. @@ -4179,13 +4355,23 @@ def __repr__() -> str Return JSON representation of the page. +#### __iter__ + +```python +def __iter__() +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L691) + +Iterate over the results in this page. + #### __getitem__ ```python def __getitem__(key: str) ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L691) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L695) Allow dictionary-like access to page attributes. @@ -4195,7 +4381,7 @@ Allow dictionary-like access to page attributes. class SearchResourceMixin(BaseMixin, Generic[SearchParamsT, ResourceT]) ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L696) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L700) Mixin for listing resources with pagination and search functionality. @@ -4220,7 +4406,7 @@ Default to match backend def search(cls: type, **kwargs: Unpack[SearchParamsT]) -> Page[ResourceT] ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L774) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L782) Search resources across the first n pages with optional filtering. @@ -4239,7 +4425,7 @@ Search resources across the first n pages with optional filtering. class GetResourceMixin(BaseMixin, Generic[GetParamsT, ResourceT]) ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L882) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L890) Mixin for getting a resource. @@ -4253,7 +4439,7 @@ def get(cls: type, **kwargs: Unpack[GetParamsT]) -> ResourceT ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L886) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L894) Retrieve a single resource by its ID (or other get parameters). @@ -4279,7 +4465,7 @@ Retrieve a single resource by its ID (or other get parameters). class DeleteResourceMixin(BaseMixin, Generic[DeleteParamsT, DeleteResultT]) ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L927) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L938) Mixin for deleting a resource. @@ -4293,7 +4479,7 @@ Default response class def build_delete_payload(**kwargs: Unpack[DeleteParamsT]) -> dict ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L932) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L943) Build the payload for the delete action. @@ -4306,7 +4492,7 @@ construction for delete operations. def build_delete_url(**kwargs: Unpack[DeleteParamsT]) -> str ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L941) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L952) Build the URL for the delete action. @@ -4325,7 +4511,7 @@ def handle_delete_response(response: Any, **kwargs: Unpack[DeleteParamsT]) -> DeleteResultT ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L958) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L969) Handle the response from a delete request. @@ -4350,7 +4536,7 @@ def before_delete(*args: Any, **kwargs: Unpack[DeleteParamsT]) -> Optional[DeleteResultT] ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L995) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1006) Optional callback called before the resource is deleted. @@ -4375,7 +4561,7 @@ def after_delete(result: Union[DeleteResultT, Exception], *args: Any, **kwargs: Unpack[DeleteParamsT]) -> Optional[DeleteResultT] ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1011) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1022) Optional callback called after the resource is deleted. @@ -4402,7 +4588,7 @@ Override this method to add custom logic after deleting. def delete(*args: Any, **kwargs: Unpack[DeleteParamsT]) -> DeleteResultT ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1035) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1046) Delete a resource. @@ -4416,7 +4602,7 @@ Delete a resource. def mark_as_deleted() -> None ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1052) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1063) Mark the resource as deleted by clearing its ID and setting deletion flag. @@ -4426,7 +4612,7 @@ Mark the resource as deleted by clearing its ID and setting deletion flag. class RunnableResourceMixin(BaseMixin, Generic[RunParamsT, ResultT]) ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1058) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1069) Mixin for runnable resources. @@ -4440,7 +4626,7 @@ Default response class def build_run_payload(**kwargs: Unpack[RunParamsT]) -> dict ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1064) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1075) Build the payload for the run action. @@ -4453,7 +4639,7 @@ parameters are dataclasses with @dataclass_json decorator. def build_run_url(**kwargs: Unpack[RunParamsT]) -> str ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1073) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1084) Build the URL for the run action. @@ -4472,7 +4658,7 @@ def handle_run_response(response: dict, **kwargs: Unpack[RunParamsT]) -> ResultT ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1094) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1105) Handle the response from a run request. @@ -4496,7 +4682,7 @@ in the 'data' field. def before_run(*args: Any, **kwargs: Unpack[RunParamsT]) -> Optional[ResultT] ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1142) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1153) Optional callback called before the resource is run. @@ -4521,7 +4707,7 @@ def after_run(result: Union[ResultT, Exception], *args: Any, **kwargs: Unpack[RunParamsT]) -> Optional[ResultT] ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1158) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1169) Optional callback called after the resource is run. @@ -4547,7 +4733,7 @@ Override this method to add custom logic after running. def run(*args: Any, **kwargs: Unpack[RunParamsT]) -> ResultT ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1181) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1192) Run the resource synchronously with automatic polling. @@ -4573,7 +4759,7 @@ Run the resource synchronously with automatic polling. def run_async(**kwargs: Unpack[RunParamsT]) -> ResultT ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1211) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1222) Run the resource asynchronously. @@ -4592,7 +4778,7 @@ Run the resource asynchronously. def poll(poll_url: str) -> ResultT ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1239) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1250) Poll for the result of an asynchronous operation. @@ -4617,7 +4803,7 @@ Poll for the result of an asynchronous operation. def on_poll(response: ResultT, **kwargs: Unpack[RunParamsT]) -> None ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1295) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1321) Hook called after each successful poll with the poll response. @@ -4635,13 +4821,16 @@ such as displaying progress updates or logging status changes. def sync_poll(poll_url: str, **kwargs: Unpack[RunParamsT]) -> ResultT ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1307) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/resource.py#L1333) Keep polling until an asynchronous operation is complete. **Arguments**: -- `poll_url` - URL to poll for results +- `poll_url` - URL to poll for results. Must be the exact URL returned + by run_async (e.g. result.url). Do not use a constructed + /sdk/runs/{id} URL — that endpoint is not supported for + agent runs. - `**kwargs` - Run parameters including timeout and wait_time @@ -4656,6 +4845,210 @@ Keep polling until an asynchronous operation is complete. --- +## aixplain.v2.rlm + +Source: `api-reference/python/aixplain/v2/rlm` + +RLM (Recursive Language Model) for aiXplain SDK v2. + +Orchestrates long-context analysis via an iterative REPL sandbox. The +orchestrator model plans and writes Python code to chunk and explore a large +context; a worker model handles per-chunk analysis via ``llm_query()`` calls +injected into the sandbox session. + +### RLMResult Objects + +```python +@dataclass_json + +@dataclass(repr=False) +class RLMResult(Result) +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/rlm.py#L146) + +Result returned by :meth:`RLM.run`. + +Extends the standard :class:`~aixplain.v2.resource.Result` with +RLM-specific fields. + +**Attributes**: + +- `iterations_used` - Number of orchestrator iterations consumed. +- `repl_logs` - Per-iteration REPL execution log (excluded from + serialization; present only on live instances). + +### RLM Objects + +```python +@dataclass_json + +@dataclass(repr=False) +class RLM(BaseResource, ToolableMixin) +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/rlm.py#L172) + +Recursive Language Model — long-context analysis via an iterative REPL sandbox. + +RLM wraps two aiXplain models: + +- An **orchestrator** (powerful, expensive): plans and writes Python code to +explore the context iteratively in a managed sandbox environment. +- A **worker** (fast, cheap): called via ``llm_query()`` inside the sandbox +to perform focused analysis on individual context chunks. + +The sandbox is an aiXplain managed Python execution environment. Each +``run()`` call gets its own isolated session (UUID), so variables persist +across REPL iterations within a single run but are cleaned up afterwards. + +RLM is a **local orchestrator** — it does not correspond to a platform +endpoint and is not saved via ``save()``. It is registered on the +:class:`~aixplain.v2.core.Aixplain` client exactly like other resources so +that credentials and URLs flow through ``self.context`` automatically. + +Example:: + +from aixplain.v2 import Aixplain + +aix = Aixplain(api_key="...") +rlm = aix.RLM( +orchestrator_id="", +worker_id="", +) + +result = rlm.run(data={ +"context": very_long_document, +"query": "What are the key findings?", +}) +print(result.data) +print(f"Completed in {result.iterations_used} iteration(s).") + +**Attributes**: + +- `orchestrator_id` - Platform model ID of the orchestrator LLM. +- ``0 - Platform model ID of the worker LLM. +- ``1 - Maximum orchestrator loop iterations (default 10). +- ``2 - Maximum wall-clock seconds per ``run()`` call (default 600). + +#### __post_init__ + +```python +def __post_init__() -> None +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/rlm.py#L261) + +Auto-assign a UUID when no id is provided. + +#### run + +```python +def run(data: Union[str, dict, pathlib.Path], + name: str = "rlm_process", + timeout: Optional[float] = None, + **kwargs: Any) -> RLMResult +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/rlm.py#L580) + +Run the RLM orchestration loop over a (potentially large) context. + +A fresh sandbox session is created for each call. The orchestrator is +called iteratively; each iteration it may execute ``repl`` code blocks in +the sandbox (outputs fed back into the conversation) and eventually declare +a final answer via ``FINAL(...)`` or ``FINAL_VAR(...)``. + +**Arguments**: + +- `data` - Input context. Accepted forms: + + - ``str`` **raw text** — used directly as context; default query applied. + - ``str`` **HTTP/HTTPS URL** — content is downloaded automatically; + ``.json`` URLs or ``application/json`` responses are parsed into a + dict/list, all other content decoded as plain text. + - ``str`` **file path** — file is read automatically; ``.json`` files are + parsed into a dict/list, all other formats read as plain text. + - ``pathlib.Path`` — resolved and read like a file-path string. + - ``dict`` — must contain ``"context"`` (required) and optionally + ``"query"`` (defaults to a generic analysis prompt). The value + of ``"context"`` itself may also be a URL, a file path, or a + ``pathlib.Path``. + +- ``1 - Identifier used in log messages. Defaults to ``"rlm_process"``. +- ``4 - Maximum wall-clock seconds. Overrides ``self.timeout`` when + provided. Defaults to ``None`` (uses ``self.timeout``). +- ``1 - Ignored; kept for API compatibility. + + +**Returns**: + + :class:``2 with: + + - ``data``: Final answer string. + - ``status``: ``"SUCCESS"`` or ``"FAILED"``. + - ``completed``: ``True``. + - ``iterations_used``: Number of orchestrator iterations consumed. + - ``repl_logs``: Per-iteration execution log (not serialized). + + +**Raises**: + +- ``9 - If ``orchestrator_id`` or ``worker_id`` are unset, + or if the orchestrator model call fails. +- `data`4 - If ``data`` is a dict missing ``"context"``, or an + unsupported type. + +#### as_tool + +```python +def as_tool() -> ToolDict +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/rlm.py#L743) + +Serialize this RLM as a tool for agent creation. + +Allows an :class:`~aixplain.v2.agent.Agent` to invoke the RLM as one of +its tools, passing a query (and optionally context) as the ``data`` +argument. + +**Returns**: + + :class:`~aixplain.v2.mixins.ToolDict` representing this RLM. + +#### run_async + +```python +def run_async(*args: Any, **kwargs: Any) -> None +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/rlm.py#L767) + +Not supported — raises :exc:`NotImplementedError`. + +#### run_stream + +```python +def run_stream(*args: Any, **kwargs: Any) -> None +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/rlm.py#L771) + +Not supported — raises :exc:`NotImplementedError`. + +#### __repr__ + +```python +def __repr__() -> str +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/rlm.py#L777) + +Return string representation of this RLM instance. + +--- + ## aixplain.v2.tool Source: `api-reference/python/aixplain/v2/tool` @@ -4671,7 +5064,7 @@ Tool resource module for managing tools and their integrations. class ToolResult(Result) ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/tool.py#L21) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/tool.py#L24) Result for a tool. @@ -4685,7 +5078,7 @@ class Tool(Model, DeleteResourceMixin[BaseDeleteParams, DeleteResult], ActionMixin) ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/tool.py#L29) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/tool.py#L32) Resource for tools. @@ -4697,57 +5090,87 @@ Inherits from Model to reuse shared attributes and functionality. Script integration +#### integration_path + +```python +@property +def integration_path() -> Optional[str] +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/tool.py#L55) + +The path of the integration (e.g. ``"aixplain/python-sandbox"``). + +Available when the ``integration`` has been resolved to an +:class:`Integration` object that carries a ``path`` attribute. +Returns ``None`` when the integration has not been resolved yet. + #### __post_init__ ```python def __post_init__() -> None ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/tool.py#L50) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/tool.py#L66) Initialize tool after dataclass creation. -Sets up default integration for utility tools if no integration is provided. -Validates integration type if provided. +#### actions + +```python +@cached_property +def actions() -> Actions +``` + +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/tool.py#L88) -#### list_actions +Collection of actions available on this tool. + +#### inputs ```python -def list_actions() -> List[Action] +@property +def inputs() ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/tool.py#L107) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/tool.py#L93) -List available actions for the tool. +Tools have multiple actions — use tool.actions['action_name'].inputs instead. -Overrides parent method to add fallback to base integration. +#### inputs -**Returns**: +```python +@inputs.setter +def inputs(value) +``` - List of Action objects available for this tool. Falls back to - integration's list_actions() if tool's own method fails. +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/tool.py#L98) -#### list_inputs +Prevent setting inputs directly on tools. + +#### list_actions ```python -def list_inputs(*actions: str) -> List["Action"] +def list_actions() -> List[ActionSpec] ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/tool.py#L126) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/tool.py#L131) -List available inputs for specified actions. +List available actions for the tool (with integration fallback). -Overrides parent method to add fallback to base integration. +#### list_inputs -**Arguments**: +```python +def list_inputs(*actions: str) -> List[ActionSpec] +``` -- `*actions` - Variable number of action names to get inputs for. - +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/tool.py#L159) -**Returns**: +List available inputs for specified actions. - List of Action objects with their input specifications. Falls back to - integration's list_inputs() if tool's own method fails. +.. deprecated:: + Use ``tool.actions['action_name'].inputs`` to discover and + configure action inputs instead. #### validate_allowed_actions @@ -4755,62 +5178,37 @@ Overrides parent method to add fallback to base integration. def validate_allowed_actions() -> None ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/tool.py#L211) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/tool.py#L330) Validate that all allowed actions are available for this tool. -Checks that: -- Integration is available (attempts lazy resolution) -- All actions in allowed_actions list exist in the integration - -Skips validation gracefully when integration cannot be resolved -(e.g. tools fetched via search/get without integration data). - -**Raises**: - -- `AssertionError` - If integration is available but actions don't match. - #### get_parameters ```python def get_parameters() -> List[dict] ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/tool.py#L238) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/tool.py#L370) Get parameters for the tool in the format expected by agent saving. -This method includes both static backend values and dynamically set values -from the ActionInputsProxy instances, ensuring agents get the current -configured action inputs. - #### as_tool ```python def as_tool() -> dict ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/tool.py#L300) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/tool.py#L421) Serialize this tool for agent creation. -This method extends the base Model.as_tool() to include tool-specific -fields like actions, which tells the backend which actions -the agent is permitted to use. - -**Returns**: - -- `dict` - A dictionary representing this tool with: - - All fields from Model.as_tool() - - actions: Explicit list of actions (filtered to allowed only) - #### run ```python def run(*args: Any, **kwargs: Unpack[ModelRunParams]) -> ToolResult ``` -[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/tool.py#L373) +[[view_source]](https://github.com/aixplain/aiXplain/blob/main/aixplain/v2/tool.py#L473) Run the tool. diff --git a/docs/llms.txt b/docs/llms.txt index 61366778..210b6630 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -2,16 +2,17 @@ > LLM-readable entrypoint for the aiXplain Python SDK v2 reference. -Use `api-reference/python/aixplain/v2/llms-full.txt` when you want the complete SDK v2 reference in one context window. +Use `llms-full.txt` when you want the complete SDK v2 reference in one context window. ## Recommended -- [SDK v2 full reference](api-reference/python/aixplain/v2/llms-full.txt): Complete concatenated aiXplain Python SDK v2 API reference. +- [SDK v2 full reference](llms-full.txt): Complete concatenated aiXplain Python SDK v2 API reference. - [SDK v2 landing page](api-reference/python/aixplain/v2/init.md): Package overview for `aixplain.v2`. ## SDK v2 Modules - [aixplain.v2](api-reference/python/aixplain/v2/init.md): aiXplain SDK v2 - Modern Python SDK for the aiXplain platform. +- [aixplain.v2.actions](api-reference/python/aixplain/v2/actions.md): Unified Actions / Inputs hierarchy for models and tools. - [aixplain.v2.agent](api-reference/python/aixplain/v2/agent.md): Agent module for aiXplain v2 SDK. - [aixplain.v2.agent_progress](api-reference/python/aixplain/v2/agent_progress.md): Agent progress tracking and display module. - [aixplain.v2.api_key](api-reference/python/aixplain/v2/api_key.md): API Key management module for aiXplain v2 API. @@ -28,6 +29,7 @@ Use `api-reference/python/aixplain/v2/llms-full.txt` when you want the complete - [aixplain.v2.mixins](api-reference/python/aixplain/v2/mixins.md): Mixins for v2 API classes. - [aixplain.v2.model](api-reference/python/aixplain/v2/model.md): Model resource for v2 API. - [aixplain.v2.resource](api-reference/python/aixplain/v2/resource.md): Resource management module for v2 API. +- [aixplain.v2.rlm](api-reference/python/aixplain/v2/rlm.md): RLM (Recursive Language Model) for aiXplain SDK v2. - [aixplain.v2.tool](api-reference/python/aixplain/v2/tool.md): Tool resource module for managing tools and their integrations. - [aixplain.v2.upload_utils](api-reference/python/aixplain/v2/upload_utils.md): File upload utilities for v2 Resource system. - [aixplain.v2.utility](api-reference/python/aixplain/v2/utility.md): Utility resource module for managing custom Python code utilities. diff --git a/generate_llms_full.py b/generate_llms_full.py index dbaa6a20..fa1d635b 100644 --- a/generate_llms_full.py +++ b/generate_llms_full.py @@ -13,7 +13,7 @@ DOCS_ROOT = REPO_ROOT / "docs" SIDEBAR_PATH = DOCS_ROOT / "api-reference/python/api_sidebar.js" LLMS_INDEX_PATH = DOCS_ROOT / "llms.txt" -LLMS_FULL_PATH = DOCS_ROOT / "api-reference/python/aixplain/v2/llms-full.txt" +LLMS_FULL_PATH = DOCS_ROOT / "llms-full.txt" TARGET_LABEL = "aixplain.v2" TARGET_PREFIX = "api-reference/python/aixplain/v2/" @@ -117,11 +117,11 @@ def build_llms_index() -> str: "", "> LLM-readable entrypoint for the aiXplain Python SDK v2 reference.", "", - "Use `api-reference/python/aixplain/v2/llms-full.txt` when you want the complete SDK v2 reference in one context window.", + "Use `llms-full.txt` when you want the complete SDK v2 reference in one context window.", "", "## Recommended", "", - "- [SDK v2 full reference](api-reference/python/aixplain/v2/llms-full.txt): Complete concatenated aiXplain Python SDK v2 API reference.", + "- [SDK v2 full reference](llms-full.txt): Complete concatenated aiXplain Python SDK v2 API reference.", "- [SDK v2 landing page](api-reference/python/aixplain/v2/init.md): Package overview for `aixplain.v2`.", "", "## SDK v2 Modules", diff --git a/pyproject.toml b/pyproject.toml index 67022d1e..394391ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ namespaces = true [project] name = "aiXplain" -version = "0.2.44" +version = "0.2.45" description = "aiXplain SDK adds AI functions to software." readme = "README.md" requires-python = ">=3.9, <4" @@ -73,5 +73,6 @@ test = [ "docker>=6.1.3", "requests-mock>=1.11.0", "pytest-mock>=3.10.0", - "pytest-rerunfailures>=16.0" + "pytest-rerunfailures>=16.0", + "pytest-xdist", ] diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 00000000..14734a43 --- /dev/null +++ b/skills/README.md @@ -0,0 +1,114 @@ +# aiXplain Skills + +A curated library of **Agent Skills** for the [aiXplain](https://aixplain.com) platform. + +A skill is a folder of instructions and reference material that teaches an AI coding agent (Claude Code, Claude Desktop) how to do a real task on aiXplain — building agents, wiring tools and integrations, deploying, and debugging — using the aiXplain SDK. Install a skill and your agent gains expert, up-to-date knowledge of how to use aiXplain correctly. + +--- + +## What is a skill? + +Each skill is a self-contained folder: + +``` +aixplain-agent-builder/ +├── SKILL.md # the entry point the agent loads +└── references/ # deeper guides loaded on demand + ├── agent-patterns.md + ├── integration-playbooks.md + └── inspectors.md +``` + +The agent reads `SKILL.md` first and pulls in the reference files only when it needs them. Every code path is verified against a specific aiXplain SDK version (noted at the top of each `SKILL.md`). + +--- + +## Available skills + +| Skill | Description | +|-------|-------------| +| [aixplain-agent-builder](./aixplain-agent-builder) | Design, deploy, run, debug, and export production agents on aiXplain — single agents, team agents, tools, OAuth integrations, and inspectors. | + +> More skills coming soon. See [Contributing](#contributing) to add one. + +--- + +## Prerequisites + +- **Python 3** and `pip` +- The aiXplain SDK: `pip install --upgrade aixplain` +- An **aiXplain API key** — create one at [studio.aixplain.com](https://studio.aixplain.com) → Settings → API Keys, then expose it: + ```bash + export AIXPLAIN_API_KEY="your_key_here" + ``` + (or add it to a local `.env` file) + +--- + +## Installation + +### Step 1 — Download the skill + +Get the skill folder onto your machine in either way. + +**Clone the whole repo:** + +```bash +git clone https://github.com/aixplain/aiXplain.git +``` + +**Or download just the skill folder:** + +1. Copy the skill folder URL from this GitHub repo (e.g. the `aixplain-agent-builder` folder). +2. Go to [download-directory.github.io](https://download-directory.github.io). +3. Paste the URL and press Enter to download the ZIP, then unzip it. + +> **Note:** `download-directory.github.io` is a third-party helper, not affiliated with or provided by aiXplain. + +### Step 2 — Install the skill + +**Claude Code (recommended).** Copy the skill folder into your Claude Code skills directory: + +```bash +cp -r aiXplain/skills/aixplain-agent-builder ~/.claude/skills/ +``` + +The skill is now available in any Claude Code session. Trigger it explicitly with `/aixplain-agent-builder`, or just describe your task ("build an aiXplain agent that…") and the agent loads it automatically. To scope a skill to a single project instead of globally, copy it into that project's `.claude/skills/` directory. + +**Claude Desktop.** Open **Settings → Capabilities → Skills**, then drag and drop the skill folder (or its `.zip`) to upload it. + +### Using with other agents (Codex, Cursor, Copilot, …) + +The auto-discovery above is a Claude Code / Claude Desktop feature — those tools read `~/.claude/skills/` and load the skill automatically. Other agents don't scan that directory, but a skill is just markdown, so you can wire it in manually using each tool's own convention: + +| Agent | How to wire it in | +|-------|-------------------| +| Codex | Reference or paste `SKILL.md` into your `AGENTS.md` | +| Cursor | Add `SKILL.md` as a rule under `.cursor/rules/` | +| GitHub Copilot | Add it to `.github/copilot-instructions.md` | +| Any chat / MCP client | Paste `SKILL.md` (and any needed `references/` files) into context | + +### Updating + +Pull the latest and re-copy: + +```bash +cd aiXplain && git pull +cp -r skills/aixplain-agent-builder ~/.claude/skills/ +``` + +--- + +## Contributing + +1. Create a folder under `skills//` with a `SKILL.md` entry point. +2. Put deep-dive material in a `references/` subfolder; keep `SKILL.md` lean and load references on demand. +3. Verify every code snippet against a known SDK version and note that version at the top of `SKILL.md`. +4. Keep skills self-contained — a user should never need to leave the skill to consult external SDK source. +5. Open a pull request into `main`. + +--- + +## License + +See the repository [LICENSE](../LICENSE). diff --git a/skills/aixplain-agent-builder/SKILL.md b/skills/aixplain-agent-builder/SKILL.md index 1a2b2e2a..8710eda3 100644 --- a/skills/aixplain-agent-builder/SKILL.md +++ b/skills/aixplain-agent-builder/SKILL.md @@ -1,12 +1,23 @@ --- name: aixplain-agent-builder -description: A skill to build, deploy, and run production-grade AI agents on aiXplain. +description: A skill to design, deploy, and run production-grade AI agents on aiXplain. metadata: {"requires": {"env": ["AIXPLAIN_API_KEY"], "bins": ["python3", "pip"]}} --- # aiXplain Agent Builder -Build, deploy, run, and manage aiXplain agents — single agents, team agents, tools, and OAuth integrations. +Design, deploy, run, and manage aiXplain agents — single agents, team agents, tools, and OAuth integrations. + +> **Last updated:** 2026-05-31 · **Verified against aiXplain SDK:** v0.2.44 (inspector API verified live) + +## Audience & Self-Containment + +This skill is used on behalf of **both non-developers and developers**. They will not read the SDK source, API reference, or GitHub. Therefore: + +- **This skill is the single source of truth.** Every code block here is verified against the SDK version above — use it as written. Do not tell the user to consult the SDK docs, GitHub, or source code. +- **Do the technical work yourself.** Write and run the code; the user should not have to. Never hand a non-developer a traceback — diagnose it, fix it, and report the outcome in plain language (what happened, what you did, what's next). +- **Explain in plain terms for non-developers**, but keep full code available for developers who want it. Lead with the result and the Studio link, not the implementation. +- **If a snippet here ever fails against the installed SDK, the skill is wrong, not the user.** Fix the skill in place, then follow the "Report aiXplain-Caused Issues" habit (see Debugging) — never redirect the user to external docs to work around it. ## How It Works @@ -26,27 +37,34 @@ This skill is verified against **SDK v0.2.44**. If the installed version differs ```python import os -from pathlib import Path from dotenv import load_dotenv from aixplain import Aixplain -env_path = Path(".env") -if env_path.exists(): - load_dotenv(env_path) -else: - env_path.write_text("# Get your key from https://platform.aixplain.com → Settings → API Keys\nAIXPLAIN_API_KEY=\n") - print(f"Created {env_path.resolve()} — paste your API key, then re-run.") - raise SystemExit(1) +load_dotenv() # loads AIXPLAIN_API_KEY from .env if present -api_key = os.getenv("AIXPLAIN_API_KEY") or os.getenv("AIXPLAIN_KEY_NUR") +api_key = os.getenv("AIXPLAIN_API_KEY") if not api_key: - print(f"No API key found. Open {env_path.resolve()} and paste your key.") - raise SystemExit(1) + raise ValueError("AIXPLAIN_API_KEY not set. Add it to your .env file or environment.") -os.environ["AIXPLAIN_API_KEY"] = api_key aix = Aixplain(api_key=api_key) ``` +> **Passing local files to agents or aiXplain assets:** Use `FileUploader` to upload a file to aiXplain S3 and get a presigned URL. Pass that URL anywhere a URL input is accepted (tools, models, integrations). `Resource.save()` is internal and not usable standalone. +> ```python +> from aixplain.v2.upload_utils import FileUploader +> uploader = FileUploader(api_key=api_key) +> file_url = uploader.upload("/path/to/file.mp3", is_temp=True, return_download_link=True) +> ``` +> +> **Downloadable links:** If the agent needs to return files the user can click and download, always set `return_download_link=True`. Without it, `upload()` returns a raw `s3://` path that is not browser-accessible. +> +> **MIME type patch for `.html` and `.zip`:** The SDK's `MimeTypeDetector` does not include these extensions — both fall back to `text/csv`, causing browsers to save files with the wrong extension. Always patch before uploading when generating these file types: +> ```python +> from aixplain.v2.upload_utils import MimeTypeDetector +> MimeTypeDetector.EXTENSION_MAPPING['.html'] = 'text/html' +> MimeTypeDetector.EXTENSION_MAPPING['.zip'] = 'application/zip' +> ``` + ## 2. Plan Before building, present a plan to the user covering: agent name, description, instructions, which tools/integrations to use, and whether it's a single or team agent. Wait for approval before proceeding. @@ -62,11 +80,20 @@ If a needed capability has no marketplace tool or integration, announce you'll b ## 3. Create Tools +**MANDATORY: Scope `allowed_actions` on every tool.** Never attach a tool to an agent without first narrowing its actions to the minimum needed for the task. Tools loaded via `Tool.get()` come with all actions enabled by default — this is over-privileged and degrades agent reasoning. Inspect `tool.actions` to see the full set, then assign `tool.allowed_actions = [...]` (or pass `allowed_actions=[...]` in the constructor) before adding to the agent. If you genuinely need all actions, state that explicitly to the user and confirm. + +```python +tool = aix.Tool.get("") +print(tool.actions) # discover available actions +tool.allowed_actions = ["search_models", "list_filters"] # scope to task +``` + Three paths, in order of preference: ```python -# Path A: Marketplace tool by ID +# Path A: Marketplace tool by ID — REMEMBER to scope allowed_actions after .get() tool = aix.Tool.get("698cda188bbb345db14ac13b") # Code Execution +tool.allowed_actions = [...] # required # Path B: Non-OAuth integration (KB, SQLite) # READ references/integration-playbooks.md for config payloads. @@ -95,6 +122,7 @@ Agents use the platform default LLM — do not specify `llm` unless the user req agent = aix.Agent( name="My Agent", description="...", instructions="...", tools=[tool], output_format="markdown", + max_tokens=6000, # set at creation, not per-run ).save() ``` @@ -106,19 +134,20 @@ After deploy, share these links with the user: - **Visual builder:** `https://studio.aixplain.com/build//schema` - **Analytics:** `https://studio.aixplain.com/dashboard/analytics/?agent=` +Default: leave `runResponseGeneration` unset. Only pass `runResponseGeneration=True` when you specifically need structured/JSON output. + ```python -# Sync -result = agent.run(query="...", executionParams={"maxTokens": 6000}, runResponseGeneration=True) +# Sync — default text output +result = agent.run(query="...") print(result.data.output) -# Async polling -import requests, time +# Sync — structured/JSON output +result = agent.run(query="...", runResponseGeneration=True) + +# Async — SDK handles polling; no manual HTTP needed ar = agent.run_async(query="...") -for _ in range(30): - time.sleep(10) - raw = requests.get(ar.url, headers={"x-api-key": api_key}).json() - if raw.get("status", "").upper() in ("SUCCESS", "FAILED"): - print(raw["data"]["output"]); break +result = agent.sync_poll(ar.url) # blocks until SUCCESS/FAILED, returns the same result shape as run() +print(result.data.output) ``` ## 6. Debugging @@ -139,6 +168,18 @@ print(stats['credits'], stats['runtime'], stats['api_calls']) Or view traces visually in Studio: `https://studio.aixplain.com/build//schema` +### Report aiXplain-Caused Issues + +When you hit a quirk or error that is **aiXplain-caused** — not a mistake in the user's code, query, or config — make it a habit to propose filing a GitHub issue at https://github.com/aixplain/aiXplain. Examples: an action advertised by an integration that always fails, an SDK method that misbehaves, a documented parameter the backend ignores, a connector requesting insufficient OAuth scopes. + +Workflow: +1. Confirm the cause is on aiXplain's side (reproduce it; rule out user input, missing scopes the user controls, rate limits, etc.). +2. Propose the issue to the user with a draft title and body. Do not post without confirmation — the repo is public. +3. **Redact personal data** from the public issue: no API keys, real email addresses, tool/agent IDs, message counts, or any account-specific data. Keep repro steps generic. +4. On approval, post with `gh issue create --repo aixplain/aiXplain` and share the issue link. + +Issue body should include: SDK version, integration/asset type, a minimal repro, the exact error, and expected vs. actual behavior. + ## Quick Asset IDs | Type | Name | ID | @@ -163,7 +204,7 @@ Or view traces visually in Studio: `https://studio.aixplain.com/build/ - `references/integration-playbooks.md` — config payloads, file upload, authoring constraints, and OAuth workflow for all integration types - `references/agent-patterns.md` — team agents, updating deployed agents, inspectors, export to Python, Code Execution vs Python Sandbox -- `references/inspector-analytics.md` — inspector policies and analytics schema +- `references/inspectors.md` — inspector policies, action values, and post-change validation ## External Links diff --git a/skills/aixplain-agent-builder/references/agent-patterns.md b/skills/aixplain-agent-builder/references/agent-patterns.md index 650d75cc..418b8042 100644 --- a/skills/aixplain-agent-builder/references/agent-patterns.md +++ b/skills/aixplain-agent-builder/references/agent-patterns.md @@ -24,7 +24,7 @@ Updatable fields: - `agent.description` — public-facing summary - `agent.tools` — add, remove, or replace tools - `agent.output_format` — `"markdown"` / `"text"` / `"json"` -- `agent.llm` — swap the underlying LLM (attribute name is `llm`, NOT `llm_id`) +- `agent.llm` — swap the underlying LLM. **Always assign to `.llm` (accepts the model ID string or a Model object). Never use `.llm_id` — that attribute exists but assigning it does NOT propagate to the save payload, so `save()` silently keeps the old model.** You can also update attached tools without detaching them — change a tool's `description`, add files to a KB tool, or change `allowed_actions`. Mutate the tool object, call `tool.save()`, and the agent picks up the change on next run. @@ -32,6 +32,7 @@ You can also update attached tools without detaching them — change a tool's `d agent = aix.Agent.get("") agent.instructions = "New system prompt..." agent.output_format = "json" +agent.llm = "" # correct — use `.llm`, NOT `.llm_id` agent.save() # Update an attached tool in place (no detach/reattach): @@ -44,31 +45,52 @@ kb_tool.save() ## Add Inspector +Inspectors attach to **team agents**. Use the **v2** inspector API (`aixplain.v2.inspector`) — it matches the v2 `Aixplain` client used everywhere in this skill. Do **not** import from `aixplain.modules.team_agent.inspector`; that path resolves to the v1 module and its `Inspector(auto=..., policy=...)` shape will fail with `ImportError`/`TypeError` under SDK v0.2.44. + ```python -from aixplain.modules.team_agent import InspectorTarget -from aixplain.modules.team_agent.inspector import Inspector, InspectorPolicy, InspectorAuto +from aixplain.v2.inspector import ( + Inspector, InspectorActionConfig, InspectorAction, + EvaluatorConfig, EvaluatorType, InspectorTarget, AUTO_DEFAULT_MODEL_ID, +) inspector = Inspector( name="Content Gate", - auto=InspectorAuto.ALIGNMENT, - model_params={"prompt": "Validate output meets policy. Fail if non-compliant."}, - policy=InspectorPolicy.ABORT, + action=InspectorActionConfig(type=InspectorAction.ABORT), # CONTINUE | RERUN | ABORT | EDIT + evaluator=EvaluatorConfig( + type=EvaluatorType.ASSET, + asset_id=AUTO_DEFAULT_MODEL_ID, # default LLM judge; or pass your own model ID + prompt="Validate output meets policy. Fail if non-compliant.", + ), + targets=[InspectorTarget.OUTPUT], # INPUT | STEPS | OUTPUT ) team.inspectors = [inspector] team.inspector_targets = [InspectorTarget.OUTPUT] team.save() ``` -After adding inspectors, validate with 3 prompts: allowed, denied, ambiguous. See `references/inspector-analytics.md` for the full validation matrix. +Notes: +- `action=RERUN` is the only type that accepts `max_retries` / `on_exhaust` on `InspectorActionConfig`; any other type raises if you set them. +- `action=EDIT` requires an `editor=EditorConfig(...)`. +- For a function-based judge instead of an LLM, use `EvaluatorConfig(type=EvaluatorType.FUNCTION, function=)`. + +After adding inspectors, validate with 3 prompts: allowed, denied, ambiguous. See `references/inspectors.md` for the full validation matrix. --- ## Export Agent to Python -1. `GET https://platform-api.aixplain.com/sdk/agents/{ID}` with `x-api-key` -2. Recurse `agents[].assetId` for subagents -3. Map API fields to SDK constructor args -4. Generate standalone `.py` with env-based key loading +Use the SDK — no raw REST. `Agent.get()` already returns the full config; serialize it and read `subagents` directly. + +```python +agent = aix.Agent.get("") +config = agent.to_dict() # full agent config as a dict +subs = agent.subagents # list of subagent objects (recurse the same way) +``` + +1. `aix.Agent.get(ID)` — load the agent (and read `.subagents` for team agents). +2. Read fields from the object or `agent.to_dict()` (name, description, instructions, output_format, llm, tools, max_tokens, inspectors). +3. Map those fields to SDK constructor args (`aix.Agent(...)`, `aix.Tool.get(...)`, inspector objects per § Add Inspector). +4. Generate a standalone `.py` that loads the key from env (`AIXPLAIN_API_KEY`) and rebuilds the agent with those args. --- diff --git a/skills/aixplain-agent-builder/references/inspector-analytics.md b/skills/aixplain-agent-builder/references/inspector-analytics.md deleted file mode 100644 index 96b409f7..00000000 --- a/skills/aixplain-agent-builder/references/inspector-analytics.md +++ /dev/null @@ -1,132 +0,0 @@ -# Inspector Analytics Contract - -Inspector policy patterns, analytics schema, and validation requirements. -Linked from the main skill — consult when adding inspectors. - ---- - -## Policy Guidelines - -- Prefer `InspectorPolicy.ABORT` for hard policy violations. -- Prefer `InspectorPolicy.ADAPTIVE` for recoverable quality issues. -- Configure `RERUN` inspectors with explicit `max_retries` and `on_exhaust` behavior. -- For existing deployments: fetch `TeamAgent`, modify `inspectors`/`inspector_targets`, call `.save()`. - ---- - -## Status Semantics - -Keep aiXplain run status as-is: `IN_PROGRESS | SUCCESS | FAILED`. -Do not overload `FAILED` for policy blocks. - -### Governance Status Enum (App-Level) - -| Status | Meaning | -|--------|---------| -| `ALLOWED` | Inspector passed, normal execution | -| `BLOCKED_BY_INSPECTOR` | Inspector aborted the run | -| `REQUIRES_HUMAN_REVIEW` | Inspector flagged for manual review | -| `INSUFFICIENT_AUTH_CONTEXT` | Missing role/scope context | -| `RESTRICTED_SCOPE` | Request exceeds allowed scope | - -### Inspector Action Values - -SDK values: `continue | rerun | abort`. - -If product UI uses `CONT|RERUN|ABORT|EDIT`, map explicitly: -- `CONT` -> `continue` -- `RERUN` -> `rerun` -- `ABORT` -> `abort` -- `EDIT` -> app-derived extension (not native inspector action) - ---- - -## Per-Run Inspector Event Fields - -Minimum fields to capture per inspector evaluation: - -| Field | Description | -|-------|-------------| -| `run_id` | Parent run identifier | -| `inspector_event_id` | Unique event ID | -| `inspector_name` | Inspector name | -| `target` | `INPUT | STEPS | OUTPUT` | -| `decision` | `continue | rerun | abort` | -| `reason_code` | Machine-readable reason | -| `severity` | `LOW | MEDIUM | HIGH | CRITICAL` | -| `final_effect` | `none | rerouted | blocked` | -| `timestamp_start_utc` | Evaluation start | -| `timestamp_end_utc` | Evaluation end | -| `latency_ms` | Evaluation duration | -| `retries_used` | Number of retries consumed | -| `governance_status` | App-level governance status | -| `access_policy` | Nullable policy object | -| `approval_status` | `DRAFT | PENDING_REVIEW | APPROVED | REJECTED` (nullable) | -| `run_total_latency_ms` | Total run duration | -| `token_or_credit_usage` | Nullable; fallback to run-level | - ---- - -## Required Mapping Rules - -| Decision | Governance Status | Final Effect | -|----------|-------------------|--------------| -| `abort` | `BLOCKED_BY_INSPECTOR` | `blocked` | -| `rerun` (successful retry) | — | `rerouted` | -| `continue` (no intervention) | — | `none` | - -Policy block can still return run `SUCCESS` with safe refusal output — treat as governance block, not runtime failure. - ---- - -## Post-Change Validation Matrix (Mandatory) - -After adding/updating inspectors, run exactly 3 prompts: - -| Test | Expected Behavior | -|------|-------------------| -| **Allowed prompt** | Continue path, compliant normal answer | -| **Denied prompt** | Blocked/denial, no restricted data/action | -| **Ambiguous prompt** | Conservative handling (deny or ask clarification) | - -For each case capture: -- `prompt` -- `expected_action` -- `observed_run_status` -- `observed_governance_status` -- `observed_output_summary` -- `pass_fail` - ---- - -## Per-Inspector KPI Cards - -When an agent is selected, show these metrics per inspector: - -| Metric | Description | -|--------|-------------| -| `inspector_id` | Unique ID | -| `inspector_name` | Display name | -| `inspector_desc` | Description | -| `target` | Input/Steps/Output | -| `policy/action_mode` | Policy type | -| `severity_model` | Severity classification | -| `evaluation_count` | Inspector evaluations (not agent runs) | -| `pass_rate_pct` | Pass rate (explicit formula required) | -| `block_rate_pct` | Abort outcomes rate | -| `rerun_rate_pct` | Rerun rate | -| `edit_rate_pct` | Edit rate (if applicable) | -| `avg_reruns_per_evaluation` | Average retries | -| `retry_exhausted_count` | Exhausted retry count | -| `avg_latency_ms` | Average evaluation latency | -| `p95_latency_ms` | 95th percentile latency | -| `avg_credits_per_evaluation` | Credit cost per evaluation | -| `last_config_change_at` | Last modification timestamp | -| `last_config_change_by` | Last modifier | -| `config_version` | Configuration version | -| `drift_7d_vs_30d_pass_delta` | 7-day vs 30-day pass rate drift | -| `drift_7d_vs_30d_block_delta` | 7-day vs 30-day block rate drift | - -### Trend Chart (Daily) - -Track per inspector: `pass_count`, `block_count`, `rerun_count`, `override_count`. diff --git a/skills/aixplain-agent-builder/references/inspectors.md b/skills/aixplain-agent-builder/references/inspectors.md new file mode 100644 index 00000000..c4ef359c --- /dev/null +++ b/skills/aixplain-agent-builder/references/inspectors.md @@ -0,0 +1,43 @@ +# Inspector Reference + +Inspector policy patterns and post-change validation for aiXplain agents. +Linked from the main skill — consult when adding inspectors. + +See `agent-patterns.md § Add Inspector` for the full v2 construction code. Use the v2 API (`aixplain.v2.inspector`), not the v1 `aixplain.modules.team_agent.inspector` path. + +--- + +## Policy Guidelines + +- Set the policy via `InspectorActionConfig(type=...)`. Valid `InspectorAction` values: `CONTINUE | RERUN | ABORT | EDIT`. +- Prefer `ABORT` for hard policy violations. +- Use `RERUN` for recoverable quality issues — it is the only action type that accepts `max_retries` and `on_exhaust` (`CONTINUE | ABORT`); setting them on any other type raises. +- `EDIT` requires an `editor=EditorConfig(...)`. +- For existing deployments: fetch the team agent, modify `inspectors`/`inspector_targets`, call `.save()`. + +--- + +## Status Semantics + +Keep aiXplain run status as-is: `IN_PROGRESS | SUCCESS | FAILED`. +Do not overload `FAILED` for policy blocks — a policy block can still return run `SUCCESS` with a safe refusal output. Treat that as a governance block, not a runtime failure. + +--- + +## Inspector Action Values + +`InspectorAction` SDK values: `continue | rerun | abort | edit`. + +--- + +## Post-Change Validation (Mandatory) + +After adding/updating inspectors, run exactly 3 prompts: + +| Test | Expected Behavior | +|------|-------------------| +| **Allowed prompt** | Continue path, compliant normal answer | +| **Denied prompt** | Blocked/denial, no restricted data/action | +| **Ambiguous prompt** | Conservative handling (deny or ask clarification) | + +For each case capture: `prompt`, `expected_action`, `observed_run_status`, `observed_output_summary`, `pass_fail`. diff --git a/skills/aixplain-agent-builder/references/integration-playbooks.md b/skills/aixplain-agent-builder/references/integration-playbooks.md index 586b58ac..b254eed9 100644 --- a/skills/aixplain-agent-builder/references/integration-playbooks.md +++ b/skills/aixplain-agent-builder/references/integration-playbooks.md @@ -43,7 +43,7 @@ Semantic search works immediately after `upsert`; `count` returns indexed docume ## 2. PostgreSQL (`693ac6e8217c7b13b480970f`) -Connect with database URL: `postgresql://username:password@host:port/database`. +Connect with database URL: `postgresql://:@:/`. - Prefer read-only DB users for analysis assistants. - Scope query actions first; enable write actions only when explicitly required. diff --git a/tests/functional/v2/test_arabic_agent.py b/tests/functional/v2/test_arabic_agent.py new file mode 100644 index 00000000..51ed4760 --- /dev/null +++ b/tests/functional/v2/test_arabic_agent.py @@ -0,0 +1,330 @@ +__author__ = "OpenAI" + +""" +Copyright 2022 The aiXplain SDK authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import re +import time +import uuid + +import pytest + +from aixplain.v2 import ( + EvaluatorConfig, + EvaluatorType, + Inspector, + InspectorAction, + InspectorActionConfig, + InspectorSeverity, + InspectorTarget, +) + +ARABIC_CHAR_RE = re.compile(r"[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]") +ARABIC_DIACRITICS_RE = re.compile(r"[\u064B-\u065F\u0670]") +DIGIT_RE = re.compile(r"[\u0660-\u06690-9]") + +MODELS = [ + pytest.param("gpt-4o", "6646261c6eb563165658bbb1", id="gpt-4o"), + pytest.param("gpt-4.1-nano", "67fd9e2bef0365783d06e2f0", id="gpt-4.1-nano"), + pytest.param("claude-opus-4.6", "698c87701239a117fd66b468", id="claude-opus-4.6"), +] + +ARABIC_QUERIES = { + "pure_arabic": "ما هي أهم القوانين التجارية في المملكة العربية السعودية؟", + "arabic_punctuation": "أولاً: العقود التجارية؛ ثانياً: الشركات، ثالثاً: الإفلاس. ما رأيك؟", + "mixed_ar_en": "اشرح لي مفهوم Due Diligence في القانون السعودي وما هي متطلبات الـ Compliance؟", + "arabic_with_numbers": "المادة ١٢٣ من نظام الشركات لعام ٢٠٢٣م تنص على أن رأس المال لا يقل عن ٥٠٠,٠٠٠ ريال.", + "arabic_special_chars": "عنوان المكتب: شارع الملك فهد، الرياض\nهاتف: +٩٦٦-١١-٢٣٤-٥٦٧٨\nبريد: info@lawfirm.sa", + "arabic_long_diacritics": "بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ — هذا نَصٌّ مُشَكَّلٌ لاختبار المعالجة.", + "arabic_legal_template": """ + بموجب هذا العقد المبرم بين: + الطرف الأول: شركة «النور للاستشارات القانونية» (سجل تجاري رقم: ١٠١٠٥٤٣٢١٠) + الطرف الثاني: مؤسسة «الأمانة للتجارة» (سجل تجاري رقم: ١٠١٠٦٧٨٩٠٠) + يتفق الطرفان على البنود التالية: + ١. مدة العقد: سنة هجرية كاملة. + ٢. قيمة العقد: ٧٥٠,٠٠٠ ريال سعودي. + ٣. التحكيم: يخضع لنظام التحكيم السعودي. + """, +} + +QUERY_EXPECTATIONS = { + "pure_arabic": {"min_arabic_ratio": 0.35, "require_keywords": ["نظام"]}, + "arabic_punctuation": {"min_arabic_ratio": 0.30, "require_keywords": ["عقود", "شركات"]}, + "mixed_ar_en": {"min_arabic_ratio": 0.20, "require_keywords_any": ["Due Diligence", "Compliance"]}, + "arabic_with_numbers": {"min_arabic_ratio": 0.20, "require_any_number": True}, + "arabic_special_chars": {"min_arabic_ratio": 0.15, "require_any_number": True}, + "arabic_long_diacritics": {"min_arabic_ratio": 0.35, "require_diacritics": True}, + "arabic_legal_template": {"min_arabic_ratio": 0.30, "require_keywords_any": ["عقد", "التحكيم", "ريال"]}, +} + +SINGLE_AGENT_DEFS = [ + { + "name": "SingleLegal", + "description": "Arabic legal advisor - single agent, Arabic instructions + input", + "instructions": ( + "ROLE: أنت مستشار قانوني سعودي متخصص في القانون التجاري.\n" + "CONSTRAINTS: أجب باللغة العربية فقط. استخدم المصطلحات القانونية الصحيحة.\n" + "OUTPUT RULES: رتّب إجابتك في نقاط مرقّمة. اذكر المراجع النظامية إن وُجدت." + ), + "queries": ["pure_arabic", "arabic_punctuation", "arabic_legal_template"], + }, + { + "name": "MixedLang", + "description": "Mixed Arabic/English agent - tests bilingual serialization", + "instructions": ( + "ROLE: You are a bilingual legal consultant fluent in Arabic and English.\n" + "CONSTRAINTS: Respond in the same language the user writes in. " + "If the query is mixed, respond in Arabic with English legal terms preserved.\n" + "OUTPUT RULES: Structured bullet points. Cite Saudi regulations by article number." + ), + "queries": ["mixed_ar_en", "arabic_with_numbers", "arabic_special_chars"], + }, + { + "name": "Diacritics", + "description": "Tests Arabic diacritics and special Unicode in instructions", + "instructions": ( + "ROLE: أنت مُدقِّقٌ لُغَوِيٌّ متخصِّصٌ في النُّصوصِ العَرَبِيَّةِ المُشَكَّلَة.\n" + "CONSTRAINTS: صحِّح أيَّ أخطاءٍ إملائيَّةٍ أو نحويَّةٍ. أضِف التَّشكيلَ الكاملَ للنَّصِّ.\n" + "OUTPUT RULES: أعِد النَّصَّ المُصَحَّحَ مع التَّشكيلِ الكاملِ." + ), + "queries": ["arabic_long_diacritics", "pure_arabic"], + }, +] + + +def _contains_arabic(text: str) -> bool: + return bool(ARABIC_CHAR_RE.search(text)) + + +def _contains_diacritics(text: str) -> bool: + return bool(ARABIC_DIACRITICS_RE.search(text)) + + +def _arabic_ratio(text: str) -> float: + visible_chars = [char for char in text if not char.isspace()] + if not visible_chars: + return 0.0 + arabic_chars = len(ARABIC_CHAR_RE.findall(text)) + return arabic_chars / len(visible_chars) + + +def _extract_output(response) -> str: + data = getattr(response, "data", None) + if data is None: + return "" + if hasattr(data, "output") and data.output is not None: + return str(data.output) + if isinstance(data, dict): + return str(data.get("output", "")) + return str(data) + + +def _extract_steps(response) -> list: + data = getattr(response, "data", None) + if data is None: + return [] + steps = getattr(data, "steps", None) or [] + if isinstance(data, dict): + steps = data.get("steps", []) or [] + return steps + + +def _assert_success_response(response) -> tuple[str, list]: + assert response is not None + assert getattr(response, "completed", None) is True + assert getattr(response, "status", "").upper() == "SUCCESS" + output = _extract_output(response) + assert output.strip(), "Expected a non-empty response output" + return output, _extract_steps(response) + + +def _assert_query_expectations(query_key: str, output: str) -> None: + expectations = QUERY_EXPECTATIONS[query_key] + assert _contains_arabic(output), f"Expected Arabic content for {query_key}" + assert "serial" not in output.lower() + assert "json parse" not in output.lower() + assert _arabic_ratio(output) >= expectations["min_arabic_ratio"], ( + f"Arabic ratio too low for {query_key}: {_arabic_ratio(output):.2f}" + ) + + for keyword in expectations.get("require_keywords", []): + assert keyword.lower() in output.lower(), f"Expected keyword '{keyword}' in {query_key} response" + + if expectations.get("require_keywords_any"): + assert any(keyword.lower() in output.lower() for keyword in expectations["require_keywords_any"]), ( + f"Expected one of {expectations['require_keywords_any']} in {query_key} response" + ) + + if expectations.get("require_any_number"): + assert DIGIT_RE.search(output), f"Expected numeric content in {query_key} response" + + if expectations.get("require_diacritics"): + assert _contains_diacritics(output), f"Expected Arabic diacritics in {query_key} response" + + +def _build_name(prefix: str, model_name: str) -> str: + return f"{prefix}-{model_name}-{int(time.time())}-{uuid.uuid4().hex[:6]}" + + +def _is_inspector_step(step: dict) -> bool: + agent_info = step.get("agent") or {} + return (agent_info.get("id") or "").lower().startswith("inspector") + + +def _is_inspector_abort_message(output: str) -> bool: + normalized = output.lower() + return "inspector detected issues" in normalized or "check your input query and inspector configuration" in normalized + + +@pytest.fixture +def resource_tracker(): + """Track created resources for reliable cleanup.""" + resources = [] + yield resources + for resource in reversed(resources): + try: + resource.delete() + except Exception: + pass + + +def _make_single_agent(client, llm_id: str, model_name: str, agent_def: dict): + agent = client.Agent( + name=_build_name(agent_def["name"], model_name), + description=agent_def["description"], + instructions=agent_def["instructions"], + llm=llm_id, + max_tokens=900, + max_iterations=4, + ) + agent.save() + return agent + + +def _make_team_agent(client, llm_id: str, model_name: str, inspectors=None): + researcher = client.Agent( + name=_build_name("ArabicResearcher", model_name), + description="باحث قانوني", + instructions=( + "ROLE: أنت باحث قانوني. ابحث في الأنظمة السعودية.\n" + "CONSTRAINTS: أجب بالعربية. اذكر أرقام المواد.\n" + "OUTPUT RULES: قدّم ملخصاً في ٣ نقاط." + ), + llm=llm_id, + ) + drafter = client.Agent( + name=_build_name("ArabicDrafter", model_name), + description="صائغ عقود", + instructions=( + "ROLE: أنت متخصص في صياغة العقود التجارية.\n" + "CONSTRAINTS: استخدم الصيغ القانونية الرسمية بالعربية.\n" + "OUTPUT RULES: صِغ الرد كمسوَّدة قانونية." + ), + llm=llm_id, + ) + researcher.save() + drafter.save() + + team_agent = client.Agent( + name=_build_name("ArabicTeamPipeline", model_name), + description="Multi-agent pipeline with Arabic", + instructions=( + "ROLE: أنت مدير مكتب محاماة. وزّع المهام على فريقك.\n" + "CONSTRAINTS: تحدث بالعربية. وجّه كل سؤال للمتخصص المناسب.\n" + "OUTPUT RULES: اجمع ردود الفريق في تقرير واحد مُنسَّق." + ), + llm=llm_id, + agents=[researcher, drafter], + inspectors=inspectors or [], + max_tokens=900, + max_iterations=4, + ) + team_agent.save() + return [researcher, drafter, team_agent] + + +def _make_output_inspector(llm_id: str, model_name: str): + return Inspector( + name=f"ArabicContentValidator-{model_name}", + severity=InspectorSeverity.HIGH, + targets=[InspectorTarget.OUTPUT], + action=InspectorActionConfig(type=InspectorAction.ABORT), + evaluator=EvaluatorConfig( + type=EvaluatorType.ASSET, + asset_id=llm_id, + prompt=( + "تحقق من أن الرد مكتوب بالعربية الفصحى وأنه يتعلق بالقانون التجاري السعودي فقط. " + "إذا كان الرد بلغة أخرى أو خارج النطاق، ارفضه." + ), + ), + ) + + +@pytest.mark.parametrize(("model_name", "llm_id"), MODELS) +def test_arabic_single_agent_variants_across_llms(client, resource_tracker, model_name, llm_id): + """Keep broad LLM coverage, but run only one representative query per agent variant.""" + representative_queries = { + "SingleLegal": "pure_arabic", + "MixedLang": "mixed_ar_en", + "Diacritics": "arabic_long_diacritics", + } + + for agent_config in SINGLE_AGENT_DEFS: + query_key = representative_queries[agent_config["name"]] + agent = _make_single_agent(client, llm_id, model_name, agent_config) + resource_tracker.append(agent) + + response = agent.run(ARABIC_QUERIES[query_key]) + output, _ = _assert_success_response(response) + _assert_query_expectations(query_key, output) + + +@pytest.mark.parametrize(("model_name", "llm_id"), MODELS) +def test_arabic_team_agent_across_llms(client, resource_tracker, model_name, llm_id): + """Use one contract-heavy query to cover the team-agent serialization path.""" + resources = _make_team_agent(client, llm_id, model_name) + resource_tracker.extend(resources) + team_agent = resources[-1] + + response = team_agent.run(ARABIC_QUERIES["arabic_legal_template"]) + output, steps = _assert_success_response(response) + _assert_query_expectations("arabic_legal_template", output) + assert steps, "Expected team-agent execution steps for Arabic team flow" + + +@pytest.mark.parametrize(("model_name", "llm_id"), MODELS) +def test_arabic_inspector_agent_across_llms(client, resource_tracker, model_name, llm_id): + """Verify the inspector actually executes on the Arabic runtime path.""" + inspector = _make_output_inspector(llm_id, model_name) + resources = _make_team_agent(client, llm_id, model_name, inspectors=[inspector]) + resource_tracker.extend(resources) + team_agent = resources[-1] + + response = team_agent.run(ARABIC_QUERIES["pure_arabic"]) + output, steps = _assert_success_response(response) + response_generator_steps = [ + step for step in steps if ((step.get("agent") or {}).get("id") or "").lower() == "response_generator" + ] + assert len(response_generator_steps) == 1, "Expected exactly one response_generator step" + + response_generator_index = steps.index(response_generator_steps[0]) + inspector_steps = [step for step in steps[response_generator_index + 1 :] if _is_inspector_step(step)] + assert inspector_steps, "Expected inspector step(s) after response_generator" + + if _is_inspector_abort_message(output): + return + + _assert_query_expectations("pure_arabic", output) diff --git a/tests/unit/rlm_test.py b/tests/unit/rlm_test.py new file mode 100644 index 00000000..b40c9d7b --- /dev/null +++ b/tests/unit/rlm_test.py @@ -0,0 +1,441 @@ +"""Unit tests for RLM context resolution, sandbox setup, credit tracking, and context window.""" + +import json +from unittest.mock import MagicMock, patch + +import pytest + +from aixplain.v1.modules.model.rlm import RLM as RLMV1 +from aixplain.v2.rlm import RLM as RLMV2, RLMResult + + +# Parametrize over both implementations +RLM_IMPLS = [ + pytest.param(RLMV1, id="v1"), + pytest.param(RLMV2, id="v2"), +] + + +# _resolve_context +class TestResolveContext: + @pytest.mark.parametrize("RLM", RLM_IMPLS) + def test_local_text_file(self, RLM, tmp_path): + p = tmp_path / "doc.txt" + p.write_text("file content", encoding="utf-8") + assert RLM._resolve_context(str(p)) == "file content" + + @pytest.mark.parametrize("RLM", RLM_IMPLS) + def test_local_json_file(self, RLM, tmp_path): + data = {"a": 1} + p = tmp_path / "data.json" + p.write_text(json.dumps(data), encoding="utf-8") + assert RLM._resolve_context(str(p)) == data + + @pytest.mark.parametrize("RLM", RLM_IMPLS) + def test_pathlib_path(self, RLM, tmp_path): + p = tmp_path / "doc.txt" + p.write_text("pathlib content", encoding="utf-8") + assert RLM._resolve_context(p) == "pathlib content" + + @pytest.mark.parametrize("RLM", RLM_IMPLS) + def test_raw_string(self, RLM): + assert RLM._resolve_context("just raw text") == "just raw text" + + @pytest.mark.parametrize("RLM", RLM_IMPLS) + def test_dict_passthrough(self, RLM): + d = {"x": 1} + assert RLM._resolve_context(d) is d + + @pytest.mark.parametrize("RLM", RLM_IMPLS) + def test_list_passthrough(self, RLM): + lst = [1, 2, 3] + assert RLM._resolve_context(lst) is lst + + @pytest.mark.parametrize("RLM", RLM_IMPLS) + def test_non_string_fallback(self, RLM): + assert RLM._resolve_context(42) == "42" + + @pytest.mark.parametrize("RLM", RLM_IMPLS) + def test_http_url_passes_through_unchanged(self, RLM): + url = "http://example.com/doc.txt" + assert RLM._resolve_context(url) == url + + @pytest.mark.parametrize("RLM", RLM_IMPLS) + def test_https_url_passes_through_unchanged(self, RLM): + url = "https://example.com/data.json" + assert RLM._resolve_context(url) == url + + +# _setup_repl — URL branch +def _make_v1_rlm() -> RLMV1: + """Minimal v1 RLM with stubbed models.""" + rlm = RLMV1.__new__(RLMV1) + rlm.api_key = "test-key" + rlm.orchestrator = MagicMock() + rlm.worker = MagicMock() + rlm.worker.url = "https://models.aixplain.com/api/v2/execute" + rlm.worker.id = "worker-id" + rlm.worker.additional_info = {} + rlm._session_id = None + rlm._sandbox_tool = None + rlm._messages = [] + rlm._used_credits = 0.0 + return rlm + + +def _make_v2_rlm() -> RLMV2: + """Minimal v2 RLM with stubbed context client.""" + rlm = RLMV2.__new__(RLMV2) + rlm.orchestrator_id = "orch-id" + rlm.worker_id = "worker-id" + rlm.max_iterations = 10 + rlm.timeout = 600.0 + rlm._session_id = None + rlm._sandbox_tool = None + rlm._orchestrator = None + rlm._worker = None + rlm._messages = [] + rlm._used_credits = 0.0 + client = MagicMock() + client.backend_url = "https://platform-api.aixplain.com" + client.api_key = "test-key" + client.model_url = "https://models.aixplain.com/api/v2/execute" + rlm.context = client + return rlm + + +class TestSetupReplURLPath: + def test_v1_url_skips_file_factory(self): + rlm = _make_v1_rlm() + sandbox_mock = MagicMock() + + with ( + patch("aixplain.factories.tool_factory.ToolFactory") as mock_tf, + patch("aixplain.factories.file_factory.FileFactory") as mock_ff, + ): + mock_tf.get.return_value = sandbox_mock + rlm._setup_repl("https://example.com/doc.txt") + + mock_ff.create.assert_not_called() + + def test_v1_url_sandbox_code_contains_url(self): + rlm = _make_v1_rlm() + sandbox_mock = MagicMock() + captured = [] + + def capture_run(inputs, action): + captured.append(inputs["code"]) + return MagicMock(used_credits=0) + + sandbox_mock.run.side_effect = capture_run + + with patch("aixplain.factories.tool_factory.ToolFactory") as mock_tf: + mock_tf.get.return_value = sandbox_mock + rlm._setup_repl("https://example.com/doc.txt") + + context_code = captured[0] + assert "https://example.com/doc.txt" in context_code + assert "_content_type" in context_code + assert "_is_json" in context_code + assert "__json.load" in context_code + + def test_v2_url_skips_file_uploader(self): + rlm = _make_v2_rlm() + sandbox_mock = MagicMock() + rlm._sandbox_tool = sandbox_mock + + with patch("aixplain.v2.rlm.FileUploader") as mock_uploader: + rlm._setup_repl("https://example.com/doc.txt") + + mock_uploader.assert_not_called() + + def test_v2_url_sandbox_code_contains_url(self): + rlm = _make_v2_rlm() + sandbox_mock = MagicMock() + rlm._sandbox_tool = sandbox_mock + captured = [] + + def capture_run(data, action): + captured.append(data["code"]) + return MagicMock(used_credits=0) + + sandbox_mock.run.side_effect = capture_run + + rlm._setup_repl("https://example.com/doc.txt") + + context_code = captured[0] + assert "https://example.com/doc.txt" in context_code + assert "_content_type" in context_code + assert "_is_json" in context_code + assert "__json.load" in context_code + + +# Credit tracking +def _sandbox_result(stdout="", stderr="", used_credits=0.0): + """Create a mock sandbox result.""" + r = MagicMock() + r.data = {"stdout": stdout, "stderr": stderr} + r.used_credits = used_credits + return r + + +def _model_response_v1(data="response text", used_credits=0.0, completed=True, status="SUCCESS"): + """Create a mock v1 model response.""" + r = MagicMock() + r.data = data + r.used_credits = used_credits + r.get = lambda k, default=None: {"completed": completed, "data": data, "status": status, "error_message": ""}.get( + k, default + ) + r.__getitem__ = lambda self_, k: {"completed": completed, "data": data, "status": status}.get(k) + return r + + +class TestV1CreditTracking: + def test_orchestrator_credits_accumulated(self): + rlm = _make_v1_rlm() + rlm._used_credits = 0.0 + rlm.orchestrator.run.return_value = _model_response_v1(used_credits=0.05) + + rlm._orchestrator_completion([{"role": "user", "content": "test"}]) + + assert rlm._used_credits == pytest.approx(0.05) + + def test_sandbox_credits_accumulated(self): + rlm = _make_v1_rlm() + rlm._used_credits = 0.0 + rlm._sandbox_tool = MagicMock() + rlm._sandbox_tool.run.return_value = _sandbox_result(used_credits=0.01) + rlm._session_id = "test-session" + + rlm._run_sandbox("print('hello')") + + assert rlm._used_credits == pytest.approx(0.01) + + def test_execute_code_credits_accumulated(self): + rlm = _make_v1_rlm() + rlm._used_credits = 0.0 + rlm._sandbox_tool = MagicMock() + rlm._sandbox_tool.run.return_value = _sandbox_result(stdout="done", used_credits=0.02) + rlm._session_id = "test-session" + + output = rlm._execute_code("x = 1\nprint('done')") + + assert "done" in output + assert rlm._used_credits == pytest.approx(0.02) + + def test_collect_llm_query_credits(self): + rlm = _make_v1_rlm() + rlm._used_credits = 1.0 + rlm._sandbox_tool = MagicMock() + rlm._session_id = "test-session" + rlm._sandbox_tool.run.return_value = _sandbox_result(stdout="0.35", used_credits=0.0) + + rlm._collect_llm_query_credits() + + assert rlm._used_credits == pytest.approx(1.35) + + def test_multiple_calls_accumulate(self): + rlm = _make_v1_rlm() + rlm._used_credits = 0.0 + rlm._session_id = "test-session" + rlm._sandbox_tool = MagicMock() + + rlm.orchestrator.run.return_value = _model_response_v1(used_credits=0.1) + rlm._orchestrator_completion([{"role": "user", "content": "a"}]) + rlm._orchestrator_completion([{"role": "user", "content": "b"}]) + + rlm._sandbox_tool.run.return_value = _sandbox_result(stdout="ok", used_credits=0.05) + rlm._execute_code("pass") + rlm._execute_code("pass") + + assert rlm._used_credits == pytest.approx(0.3) + + +class TestV2CreditTracking: + def test_orchestrator_credits_accumulated(self): + rlm = _make_v2_rlm() + rlm._used_credits = 0.0 + mock_model = MagicMock() + resp = MagicMock() + resp.completed = True + resp.status = "SUCCESS" + resp.data = "answer" + resp.used_credits = 0.07 + mock_model.run.return_value = resp + rlm._orchestrator = mock_model + + rlm._orchestrator_completion([{"role": "user", "content": "test"}]) + + assert rlm._used_credits == pytest.approx(0.07) + + def test_sandbox_credits_accumulated(self): + rlm = _make_v2_rlm() + rlm._used_credits = 0.0 + sandbox = MagicMock() + sandbox.run.return_value = _sandbox_result(used_credits=0.03) + rlm._session_id = "test-session" + + rlm._run_sandbox(sandbox, "print('hi')") + + assert rlm._used_credits == pytest.approx(0.03) + + def test_execute_code_credits_accumulated(self): + rlm = _make_v2_rlm() + rlm._used_credits = 0.0 + sandbox = MagicMock() + sandbox.run.return_value = _sandbox_result(stdout="done", used_credits=0.04) + rlm._sandbox_tool = sandbox + rlm._session_id = "test-session" + + output = rlm._execute_code("print('done')") + + assert "done" in output + assert rlm._used_credits == pytest.approx(0.04) + + def test_collect_llm_query_credits(self): + rlm = _make_v2_rlm() + rlm._used_credits = 2.0 + sandbox = MagicMock() + sandbox.run.return_value = _sandbox_result(stdout="0.50", used_credits=0.0) + rlm._sandbox_tool = sandbox + rlm._session_id = "test-session" + + rlm._collect_llm_query_credits() + + assert rlm._used_credits == pytest.approx(2.50) + + def test_used_credits_field_on_rlm_result(self): + result = RLMResult(status="SUCCESS", completed=True, data="answer") + result.used_credits = 1.23 + result.iterations_used = 5 + + assert result.used_credits == pytest.approx(1.23) + serialized = result.to_dict() + assert serialized["usedCredits"] == pytest.approx(1.23) + + +class TestLlmQueryCodeCreditsTracking: + def test_v1_llm_query_code_accumulates_credits(self): + rlm = _make_v1_rlm() + sandbox_mock = MagicMock() + captured = [] + + def capture_run(inputs, action): + captured.append(inputs["code"]) + return MagicMock(used_credits=0) + + sandbox_mock.run.side_effect = capture_run + + with ( + patch("aixplain.factories.tool_factory.ToolFactory") as mock_tf, + patch("aixplain.factories.file_factory.FileFactory") as mock_ff, + ): + mock_tf.get.return_value = sandbox_mock + mock_ff.create.return_value = "https://storage.example.com/ctx.txt" + rlm._setup_repl("raw text context") + + llm_query_code = captured[-1] + assert "_total_llm_query_credits" in llm_query_code + assert "global _total_llm_query_credits" in llm_query_code + assert "usedCredits" in llm_query_code + + def test_v2_llm_query_code_accumulates_credits(self): + rlm = _make_v2_rlm() + sandbox_mock = MagicMock() + captured = [] + + def capture_run(data, action): + captured.append(data["code"]) + return MagicMock(used_credits=0) + + sandbox_mock.run.side_effect = capture_run + rlm._sandbox_tool = sandbox_mock + + with patch("aixplain.v2.rlm.FileUploader") as mock_uploader: + uploader_instance = MagicMock() + uploader_instance.upload.return_value = "https://storage.example.com/ctx.txt" + mock_uploader.return_value = uploader_instance + rlm._setup_repl("raw text context") + + llm_query_code = captured[-1] + assert "_total_llm_query_credits" in llm_query_code + assert "global _total_llm_query_credits" in llm_query_code + assert "usedCredits" in llm_query_code + + +# Worker context window +class TestV1WorkerContextWindow: + def test_returns_formatted_k_tokens(self): + rlm = _make_v1_rlm() + rlm.worker.additional_info = {"attributes": [{"name": "max_context_length", "code": "128000"}]} + assert rlm._get_worker_context_window() == "128K tokens" + + def test_returns_formatted_m_tokens(self): + rlm = _make_v1_rlm() + rlm.worker.additional_info = {"attributes": [{"name": "max_context_length", "code": "1048576"}]} + assert rlm._get_worker_context_window() == "1.0M tokens" + + def test_returns_small_token_count(self): + rlm = _make_v1_rlm() + rlm.worker.additional_info = {"attributes": [{"name": "max_context_length", "code": "512"}]} + assert rlm._get_worker_context_window() == "512 tokens" + + def test_fallback_when_no_attributes(self): + rlm = _make_v1_rlm() + rlm.worker.additional_info = {} + assert rlm._get_worker_context_window() == "a large context window" + + def test_fallback_when_attribute_missing(self): + rlm = _make_v1_rlm() + rlm.worker.additional_info = {"attributes": [{"name": "other_attr", "code": "100"}]} + assert rlm._get_worker_context_window() == "a large context window" + + def test_non_numeric_returns_raw_string(self): + rlm = _make_v1_rlm() + rlm.worker.additional_info = {"attributes": [{"name": "max_context_length", "code": "unlimited"}]} + assert rlm._get_worker_context_window() == "unlimited" + + +class TestV2WorkerContextWindow: + def test_returns_formatted_k_tokens(self): + rlm = _make_v2_rlm() + mock_worker = MagicMock() + mock_worker.attributes = {"max_context_length": "200000"} + rlm._worker = mock_worker + assert rlm._get_worker_context_window() == "200K tokens" + + def test_returns_formatted_m_tokens(self): + rlm = _make_v2_rlm() + mock_worker = MagicMock() + mock_worker.attributes = {"max_context_length": "2000000"} + rlm._worker = mock_worker + assert rlm._get_worker_context_window() == "2.0M tokens" + + def test_fallback_when_no_attributes(self): + rlm = _make_v2_rlm() + mock_worker = MagicMock() + mock_worker.attributes = {} + rlm._worker = mock_worker + assert rlm._get_worker_context_window() == "a large context window" + + def test_fallback_when_attributes_none(self): + rlm = _make_v2_rlm() + mock_worker = MagicMock() + mock_worker.attributes = None + rlm._worker = mock_worker + assert rlm._get_worker_context_window() == "a large context window" + + def test_non_numeric_returns_raw_string(self): + rlm = _make_v2_rlm() + mock_worker = MagicMock() + mock_worker.attributes = {"max_context_length": "very_large"} + rlm._worker = mock_worker + assert rlm._get_worker_context_window() == "very_large" + + def test_integer_attribute_value(self): + rlm = _make_v2_rlm() + mock_worker = MagicMock() + mock_worker.attributes = {"max_context_length": 32000} + rlm._worker = mock_worker + assert rlm._get_worker_context_window() == "32K tokens" diff --git a/tests/unit/team_agent/team_agent_test.py b/tests/unit/team_agent/team_agent_test.py index 55ae966d..162927ae 100644 --- a/tests/unit/team_agent/team_agent_test.py +++ b/tests/unit/team_agent/team_agent_test.py @@ -337,6 +337,7 @@ def test_team_agent_serialization_completeness(): "instructions", "outputFormat", "expectedOutput", + "contextOverflowStrategy", } assert set(team_dict.keys()) == required_fields diff --git a/tests/unit/v2/test_agent_progress.py b/tests/unit/v2/test_agent_progress.py new file mode 100644 index 00000000..8297fd21 --- /dev/null +++ b/tests/unit/v2/test_agent_progress.py @@ -0,0 +1,63 @@ +"""Unit tests for the v2 agent progress formatter.""" + +from aixplain.v2.agent_progress import AgentProgressTracker + + +def _tracker() -> AgentProgressTracker: + """Create a tracker with a no-op poll function.""" + return AgentProgressTracker(poll_func=lambda _: None) + + +def test_format_token_usage_inline_uses_arrow_style(): + """Token usage should render with input/output arrows and total in parentheses.""" + tracker = _tracker() + + text = tracker._format_token_usage_inline( + { + "input_tokens": 510, + "output_tokens": 536, + "total_tokens": 1046, + } + ) + + assert text == "↓510 ↑536 (1046)" + + +def test_format_step_line_includes_arrow_style_tokens(): + """Step lines should include the compact token summary in logs/status output.""" + tracker = _tracker() + + line = tracker._format_step_line( + { + "agent": {"name": "Responder"}, + "unit": {"name": "Final Answer", "type": "llm"}, + "api_calls": 1, + "used_credits": 0.123456, + "input_tokens": 510, + "output_tokens": 536, + "total_tokens": 1046, + }, + step_idx=0, + icon="✓", + step_elapsed=1.23, + show_timing=True, + is_complete=True, + ) + + assert "· ↓510 ↑536 (1046) ·" in line + + +def test_completion_message_includes_arrow_style_totals(capsys): + """Completion summaries should use the same token formatting.""" + tracker = _tracker() + tracker._format = "logs" + tracker._total_start_time = tracker._now() + tracker._total_api_calls = 3 + tracker._total_credits = 0.654321 + tracker._total_input_tokens = 510 + tracker._total_output_tokens = 536 + + tracker._print_completion_message("SUCCESS", [{}, {}]) + + captured = capsys.readouterr().out + assert "↓510 ↑536 (1046)" in captured diff --git a/tests/unit/v2/test_inspector.py b/tests/unit/v2/test_inspector.py index 9e0a4970..a2607460 100644 --- a/tests/unit/v2/test_inspector.py +++ b/tests/unit/v2/test_inspector.py @@ -12,6 +12,8 @@ EvaluatorType, EvaluatorConfig, EditorConfig, + PrebuiltInspector, + is_prebuilt_inspector, ) @@ -293,3 +295,131 @@ def test_optional_fields_omitted(self): assert "description" not in payload assert "severity" not in payload assert "editor" not in payload + + +# --------------------------------------------------------------------------- +# PrebuiltInspector +# --------------------------------------------------------------------------- + + +class TestPrebuiltInspector: + def test_prompt_injection_guard_defaults(self): + inspector = PrebuiltInspector.prompt_injection_guard() + payload = inspector.to_dict() + assert payload == {"presetId": "prompt_injection_guard"} + + def test_pii_redaction_defaults(self): + inspector = PrebuiltInspector.pii_redaction() + payload = inspector.to_dict() + assert payload == {"presetId": "pii_redaction"} + + def test_prompt_injection_guard_with_overrides(self): + inspector = PrebuiltInspector.prompt_injection_guard( + targets=[InspectorTarget.OUTPUT], + severity=InspectorSeverity.CRITICAL, + name="my_guard", + description="Custom guard", + ) + payload = inspector.to_dict() + assert payload == { + "presetId": "prompt_injection_guard", + "name": "my_guard", + "description": "Custom guard", + "targets": ["output"], + "severity": "critical", + } + + def test_pii_redaction_with_action_override(self): + inspector = PrebuiltInspector.pii_redaction( + action={"type": "abort"}, + targets=[InspectorTarget.INPUT, InspectorTarget.OUTPUT], + ) + payload = inspector.to_dict() + assert payload == { + "presetId": "pii_redaction", + "targets": ["input", "output"], + "action": {"type": "abort"}, + } + + def test_unknown_preset_rejected(self): + with pytest.raises(ValueError, match="Unknown inspector preset"): + PrebuiltInspector(preset_id="nonexistent") + + def test_unsupported_action_rejected(self): + with pytest.raises(ValueError, match="not supported by preset"): + PrebuiltInspector.prompt_injection_guard(action={"type": "edit"}) + + def test_pii_redaction_rejects_rerun(self): + with pytest.raises(ValueError, match="not supported by preset"): + PrebuiltInspector.pii_redaction(action={"type": "rerun"}) + + def test_targets_normalized_from_enums(self): + inspector = PrebuiltInspector.pii_redaction( + targets=[InspectorTarget.STEPS, InspectorTarget.OUTPUT], + ) + assert inspector.targets == ["steps", "output"] + + def test_targets_normalized_from_strings(self): + inspector = PrebuiltInspector.prompt_injection_guard( + targets=["INPUT", "Steps"], + ) + assert inspector.targets == ["input", "steps"] + + def test_from_dict_roundtrip(self): + original = PrebuiltInspector.prompt_injection_guard( + targets=[InspectorTarget.INPUT], + severity=InspectorSeverity.HIGH, + name="custom_name", + ) + restored = PrebuiltInspector.from_dict(original.to_dict()) + assert restored.to_dict() == original.to_dict() + + def test_from_dict_minimal(self): + data = {"presetId": "pii_redaction"} + inspector = PrebuiltInspector.from_dict(data) + assert inspector.preset_id == "pii_redaction" + assert inspector.targets is None + assert inspector.action is None + + def test_list_presets_returns_both(self): + presets = PrebuiltInspector.list_presets() + assert "prompt_injection_guard" in presets + assert "pii_redaction" in presets + assert presets["prompt_injection_guard"]["category"] == "protection" + assert presets["pii_redaction"]["category"] == "redaction" + + def test_list_presets_contains_expected_fields(self): + presets = PrebuiltInspector.list_presets() + for preset_id, meta in presets.items(): + assert "name" in meta + assert "description" in meta + assert "default_targets" in meta + assert "default_action" in meta + assert "supported_actions" in meta + + +# --------------------------------------------------------------------------- +# is_prebuilt_inspector helper +# --------------------------------------------------------------------------- + + +class TestIsPrebuiltInspector: + def test_prebuilt_instance(self): + assert is_prebuilt_inspector(PrebuiltInspector.pii_redaction()) is True + + def test_preset_dict(self): + assert is_prebuilt_inspector({"presetId": "prompt_injection_guard"}) is True + + def test_regular_inspector_instance(self): + inspector = Inspector( + name="custom", + action=InspectorActionConfig(type=InspectorAction.ABORT), + evaluator=EvaluatorConfig(type=EvaluatorType.ASSET, asset_id="x"), + ) + assert is_prebuilt_inspector(inspector) is False + + def test_regular_dict(self): + assert is_prebuilt_inspector({"name": "foo", "action": {"type": "abort"}}) is False + + def test_none(self): + assert is_prebuilt_inspector(None) is False