Skip to main content

Lets AI agents author and run SmartMDAO MDA/MDAO pipelines, with validation and traceability.

Project description

smartmdao-agents

CI

Turns a plain-English engineering task into a validated, sandboxed, auditable run of SmartMDAO code. Hand it a task description and a way to call your LLM; it hands back a resolved result, or a structured reason why not.

Status: Phase 1 (MDA) and Phase 2 (MDAO) complete. A separate project from smartmdao on purpose - smartmdao stays a general-purpose, agent-agnostic runtime; this repo is the fast- moving orchestration layer on top (prompts, validation rules, sandboxing).

How it works

This repo never calls an LLM itself - no SDK dependency, no provider lock-in. You supply call_model: a plain function taking a prompt string and returning the model's raw text response. Everything else - building the prompt from the curated reference doc, extracting code from the response, validating it, running it in a sandboxed subprocess, and retrying with the error fed back on failure - is handled for you.

flowchart TD
    subgraph caller["You supply"]
        callmodel["call_model(prompt) -> str\na thin wrapper around your LLM SDK"]
    end

    subgraph pkg["run_agent_task(task, inputs, call_model=...)"]
        extract["build prompt → call_model → extract code"]
        runpipeline["validate (AST allowlist)\n→ sandboxed subprocess\n→ JSON audit record"]
        retry["on failure: retry with\nthe error fed back"]

        extract --> runpipeline
        runpipeline -->|"status != success,\nattempts remaining"| retry
        retry --> extract
    end

    callmodel <-.-> extract
    runpipeline --> result["RunRecord: status, outputs, error"]
from smartmdao_agents import run_agent_task

def call_model(prompt: str) -> str:
    return my_llm_client.complete(prompt)  # wrap whatever you're already using

record = run_agent_task(
    "compute shipping cost from weight and distance",
    {"weight_kg": 4.0, "distance_km": 320, "express": True},
    call_model=call_model,
    max_attempts=3,
)
record.status   # "success" | "validation_failed" | "execution_failed" | "timeout"
record.outputs  # every resolved variable, or None on failure

Already have code (not fresh off an LLM call) and just want it validated/sandboxed/recorded? run_agent_pipeline(code, inputs) skips the prompt/extract/retry machinery - run_agent_task is built on top of it, not a replacement. It's also what the CLI uses:

smartmdao-agents run --file pipeline.py --inputs '{"width": 2.0}'

Examples

Both of these are real: the exact task description that was sent, the exact code an isolated model produced from it - nothing hand-written, nothing edited - and the verified result of actually running that code. See docs/dogfooding.md for the full methodology and all four blind-test results (these two, plus a linear-MDA and an unconstrained-MDAO task).

Non-numeric MDA — the capability that differentiates this from OpenMDAO/GEMSEO

Write a smartmdao pipeline that resolves which spacecraft sensors can be enabled within a power budget. There is a fixed candidate list of sensor names and a dict mapping each sensor name to its power draw (define these as module-level constants). One step should propose enabling sensors one at a time, in a fixed deterministic order, as long as the cumulative power draw of already-enabled sensors stays within a power_budget input (float), stopping once the next sensor would exceed the budget. A second step should review/pass through the enabled set unchanged. Converge these two steps to a fixed point using smartmdao's non-numeric convergence support. Accept power_budget as the only external input.

@pipeline.step(outputs=["selection"])
def a_propose(
    power_budget: float,
    reviewed_selection: SensorSelection = SensorSelection(enabled=frozenset()),
) -> SensorSelection:
    enabled = set(reviewed_selection.enabled)
    total = sum(SENSOR_POWER_DRAW[name] for name in enabled)
    for name in CANDIDATE_SENSORS:
        if name in enabled:
            continue
        draw = SENSOR_POWER_DRAW[name]
        if total + draw <= power_budget:
            enabled.add(name)
            total += draw
        else:
            break
    return SensorSelection(enabled=frozenset(enabled))

@pipeline.step(outputs=["reviewed_selection"])
def b_review(selection: SensorSelection) -> SensorSelection:
    return selection

Full fixture: tests/fixtures/safe/sensor_power_budget.py. With power_budget=50.0, converges in 2 iterations to all 5 sensors enabled - HybridSolver detected the a_propose ↔ b_review cycle automatically, on a coupling variable that's never a number anywhere.

Constrained MDAO

Write a smartmdao MDAO problem for designing an open-top rectangular box (no lid) with a fixed height of 10. Design variables are width and depth. Minimize the total material used (surface area: the bottom plus the four sides), subject to the box holding at least a volume of 200 (width * depth * height >= 200).

@pipeline.step(outputs=["surface_area", "volume_slack"])
def evaluate_box(width, depth, height):
    bottom = width * depth
    sides = 2 * (width * height) + 2 * (depth * height)
    surface_area = bottom + sides
    volume = width * depth * height
    return surface_area, volume - MIN_VOLUME  # a slack, not the raw volume

evaluator = PipelineEvaluator(pipeline, design_vars=["width", "depth"], constants={"height": HEIGHT})
problem = OptimizationProblem(
    evaluator=evaluator,
    initial_guess=[5.0, 5.0],
    bounds=[(0.1, 100.0), (0.1, 100.0)],
    objective="surface_area",
    constraints=[ConstraintSpec(name="volume_slack", kind="ineq", multiplier=1.0)],
)

Full fixture: tests/fixtures/safe/mdao_constrained_box.py. Converges to width = depth ≈ 4.472, matching the textbook-optimal square-base ratio (w = d = √(V/h)) - an independent check that the answer is actually right, not just that nothing crashed.

Known gotchas

Found by the dogfooding in docs/dogfooding.md, fixed in reference.md, and pinned down with regression tests so they can't silently regress:

  • Cycle execution order. Cyclic steps run in alphabetical order by function name, every iteration. A step seeding a cycle via a Python default has to sort before the step consuming its output, or the first iteration fails.
  • OptimizationProblem.objective defaults silently to the literal string "objective" - fails with an opaque KeyError if you forget to set it to a real output name.
  • A constraint must encode a slack (value - threshold), never the raw value. Constraining the raw value only enforces value >= 0 - the optimizer will silently ignore any real threshold and still report success. This was the most severe finding: a wrong answer with no error anywhere.

Roadmap

  • Phase 1 (done) — MDA, protocol-agnostic: curated reference doc, AST validator, subprocess sandbox with rlimits/timeout, JSON audit trail, one entry point + CLI.
  • Phase 2 (done) — MDAO as an additive toggle: problem: OptimizationProblem auto-detected alongside pipeline: Pipeline, backend selectable per call (scipy/openturns).
  • Caller-side glue (done)run_agent_task + the retry-with-error-feedback loop, behind a single caller-supplied call_model function.
  • Phase 3 (not started) — protocol adapters (MCP, LangChain, etc.) over the same contract.

Development

uv sync
uv run pytest        # 100% coverage enforced via pytest.ini

Depends on smartmdao from PyPI (>=1.5.0, the first release with non-numeric convergence support). License: MIT.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

smartmdao_agents-0.1.0.tar.gz (20.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

smartmdao_agents-0.1.0-py3-none-any.whl (25.7 kB view details)

Uploaded Python 3

File details

Details for the file smartmdao_agents-0.1.0.tar.gz.

File metadata

  • Download URL: smartmdao_agents-0.1.0.tar.gz
  • Upload date:
  • Size: 20.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.14

File hashes

Hashes for smartmdao_agents-0.1.0.tar.gz
Algorithm Hash digest
SHA256 95fc301d1f6b3fede2d91242c6930a0298a9bdca3602aa2a91597589c4682ad3
MD5 03a8da1f4abfb090c99b520b0462424d
BLAKE2b-256 323ab8bec85bb653ddfe972adeb20b3c6045c06ffadcd65d2feff6c41068b8fd

See more details on using hashes here.

File details

Details for the file smartmdao_agents-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for smartmdao_agents-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5987e8bb5835de86473a2c21a9578983df887ce1369b53d55db22402d716c6c4
MD5 1e5854bbe9ecc5be5800b68295a896f8
BLAKE2b-256 6f32c6a0d23bddf18ebb575ad1472e72786b308de00b556b51e742caf66614cb

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page