Source code for pycsamt.agents.master

# Author: LKouadio <etanoyau@gmail.com>
# License: LGPL-3.0
"""One-line front door to the pyCSAMT agent stack.

:class:`AgentMaster` is a thin façade over
:class:`~pycsamt.agents.WorkflowOrchestratorAgent`: one object, one
``run()`` call from a plain-language request to an executed agent
chain.  It exists so the friendly entry point advertised across the
docs works verbatim::

    from pycsamt.agents import AgentMaster

    master = AgentMaster(provider="anthropic")
    report = master.run(
        "Load data/edi/, flag stations with RMS > 2, build an Occam2D "
        "input for profile L22, launch inversion, and produce a PDF report."
    )

Everything heavier (the orchestrator, LLM clients) is imported lazily
on first use, so ``from pycsamt.agents import AgentMaster`` stays cheap
and free of circular imports.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from ..api.property import PyCSAMTObject

if TYPE_CHECKING:  # pragma: no cover - typing only
    from ._base import AgentResult
    from .orchestrator import WorkflowOrchestratorAgent

__all__ = ["AgentMaster"]

# Friendly names accepted in addition to the canonical BaseAgent
# providers ({"claude", "openai", "gemini", "deepseek", "minimax"}).
_PROVIDER_ALIASES = {
    "anthropic": "claude",
    "claude": "claude",
    "openai": "openai",
    "gpt": "openai",
    "gemini": "gemini",
    "google": "gemini",
    "deepseek": "deepseek",
    "minimax": "minimax",
}


[docs] class AgentMaster(PyCSAMTObject): """Plain-language entry point to the agent workflows. Parameters ---------- provider : str, default ``"claude"`` LLM provider. Friendly aliases are accepted: ``"anthropic"`` → ``"claude"`` and ``"google"`` → ``"gemini"``. Without an API key the agents fall back to the rule-based (regex/keyword) path, so ``AgentMaster()`` works offline. api_key : str, optional Provider API key. When omitted, the provider's environment variable is used if set; otherwise the rule-based fallback runs at zero cost. model : str, optional Provider model override (defaults per provider). default_workflow : str, default ``"qc"`` Workflow used when a request cannot be classified. Examples -------- Plan first (no files touched), then execute: >>> from pycsamt.agents import AgentMaster >>> master = AgentMaster(provider="anthropic") >>> plan = master.plan("QC the EDI files and prepare a short report", ... data_path="data/edi/") >>> plan["workflow_type"] # doctest: +SKIP 'qc' >>> report = master.run( ... "Load data/edi/, flag stations with RMS > 2, build an Occam2D " ... "input for profile L22, launch inversion, and produce a PDF " ... "report." ... ) # doctest: +SKIP See Also -------- pycsamt.agents.WorkflowOrchestratorAgent : The dispatcher this façade drives; use it directly for structured workflow configurations. """ __repr_fields__ = ("provider", "default_workflow") def __init__( self, provider: str = "claude", *, api_key: str | None = None, model: str | None = None, default_workflow: str = "qc", ) -> None: key = str(provider).strip().lower() if key not in _PROVIDER_ALIASES: raise ValueError( "provider must be one of " f"{sorted(set(_PROVIDER_ALIASES))}, got {provider!r}" ) self.provider = _PROVIDER_ALIASES[key] self.default_workflow = default_workflow self._api_key = api_key self._model = model self._orchestrator: WorkflowOrchestratorAgent | None = None # ------------------------------------------------------------------
[docs] @property def orchestrator(self) -> WorkflowOrchestratorAgent: """The lazily-built orchestrator behind this façade.""" if self._orchestrator is None: # Local import: keeps module import light and cycle-free. from .orchestrator import ( WorkflowOrchestratorAgent, ) self._orchestrator = WorkflowOrchestratorAgent( api_key=self._api_key, model=self._model, llm_provider=self.provider, default_workflow=self.default_workflow, ) return self._orchestrator
# ------------------------------------------------------------------
[docs] def run( self, request: str, *, data_path: str | None = None, output_dir: str | None = None, dry_run: bool = False, **extra: Any, ) -> AgentResult: """Classify *request* into a workflow and execute the chain. Parameters ---------- request : str Plain-language description of what to do. Paths mentioned in the text are extracted when possible; pass ``data_path`` / ``output_dir`` explicitly for scripts and CI. data_path : str, optional Survey input (EDI/AVG/J directory or file). output_dir : str, optional Where products (figures, inputs, reports) are written. dry_run : bool, default False Preview the selected chain without reading or writing. **extra Additional orchestrator payload fields, passed through. Returns ------- AgentResult Status, per-step outputs, reasoning, and cost tracking. """ payload: dict[str, Any] = { "request": str(request), "dry_run": bool(dry_run), **extra, } if data_path is not None: payload["data_path"] = str(data_path) if output_dir is not None: payload["output_dir"] = str(output_dir) return self.orchestrator.execute(payload)
[docs] def plan(self, request: str, **kwargs: Any) -> AgentResult: """Shortcut for :meth:`run` with ``dry_run=True``.""" kwargs["dry_run"] = True return self.run(request, **kwargs)