Lets AI agents author and run SmartMDAO MDA/MDAO pipelines, with validation and traceability.
Project description
smartmdao-agents
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_budgetinput (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. Acceptpower_budgetas 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
widthanddepth. 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.objectivedefaults silently to the literal string"objective"- fails with an opaqueKeyErrorif 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 enforcesvalue >= 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: OptimizationProblemauto-detected alongsidepipeline: Pipeline, backend selectable per call (scipy/openturns). - Caller-side glue (done) —
run_agent_task+ the retry-with-error-feedback loop, behind a single caller-suppliedcall_modelfunction. - 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
95fc301d1f6b3fede2d91242c6930a0298a9bdca3602aa2a91597589c4682ad3
|
|
| MD5 |
03a8da1f4abfb090c99b520b0462424d
|
|
| BLAKE2b-256 |
323ab8bec85bb653ddfe972adeb20b3c6045c06ffadcd65d2feff6c41068b8fd
|
File details
Details for the file smartmdao_agents-0.1.0-py3-none-any.whl.
File metadata
- Download URL: smartmdao_agents-0.1.0-py3-none-any.whl
- Upload date:
- Size: 25.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5987e8bb5835de86473a2c21a9578983df887ce1369b53d55db22402d716c6c4
|
|
| MD5 |
1e5854bbe9ecc5be5800b68295a896f8
|
|
| BLAKE2b-256 |
6f32c6a0d23bddf18ebb575ad1472e72786b308de00b556b51e742caf66614cb
|