Prompt Strategy Comparison Walkthrough#
This walkthrough demonstrates the umbrella package doing real work with a live
model-backed agent while following the comparison-study recipe/reporting APIs
published in the pinned sibling-library releases. It uses a real packaged
problem from design_research.problems, a managed
prompt-mode design_research.agents.Workflow,
design_research.agents.PromptWorkflowAgent, the
design_research.experiments.build_strategy_comparison_study scaffold, and
the newer condition-comparison helpers from design_research.analysis.
What This Covers#
resolves a real packaged problem through
design_research.problemsbuilds the study from
design_research.experiments.build_strategy_comparison_studywith a recipe-first benchmark bundle containing a random baseline, a neutral prompt, and a profit-focused promptruns the live study through
design_research.experiments.run_studyexports the canonical study artifacts plus a markdown summary report built from
render_markdown_summary,render_methods_scaffold,render_codebook, andrender_significance_briefvalidates the exported event rows through
design_research.analysiscomputes ordered one-sided condition-pair permutation tests from the exported
runs.csvandevaluations.csvtables viabuild_condition_metric_tableandcompare_condition_pairs
Package Alignment#
This local walkthrough intentionally tracks the pinned package APIs
from design-research-agents, design-research-experiments, and
design-research-analysis. If you run it against older releases of those
sibling packages, it will fail fast with a clear upgrade message instead of
silently drifting from the new workflow/recipe/reporting surface.
During local development, the umbrella test harness can point subprocess runs at adjacent sibling worktrees so the examples stay validated against the same public APIs owned by the sibling libraries themselves.
Run It#
python -m pip install "design-research-agents[llama_cpp]==0.6.0"
make run-example
Optionally point the walkthrough at a specific local GGUF file:
export LLAMA_CPP_MODEL=/path/to/model.gguf
make run-example
The default configuration uses 50 replicates per condition. To choose a different sample size, set the replicate count explicitly:
export PROMPT_STUDY_REPLICATES=12
make run-example
Maintainers can use make live-smoke-llama-cpp for a smoke-sized semantic
check with two replicates per condition. The smoke target still exercises both
model-backed prompt strategies; it does not replace the full walkthrough when
you need study-scale results.
The example writes canonical exports to
artifacts/examples/prompt_strategy_comparison_study and writes a markdown
summary report to
artifacts/examples/prompt_strategy_comparison_study/artifacts/prompt_strategy_summary.md.
It prints condition means, a condition-comparison brief, a significance brief,
the summary-report path, exported artifact paths, and the event-table
validation summary. The script intentionally has no deterministic fallback path
for the live-agent conditions: it expects a real llama.cpp runtime.
If LLAMA_CPP_MODEL is not set, the client falls back to its built-in model
defaults and Hugging Face repo settings. The Agents llama_cpp extra
includes that download client, so the first run may download a model before the
walkthrough executes. The managed client allows up to five minutes for that
first startup. On an unusually slow connection, set
LLAMA_CPP_STARTUP_TIMEOUT_SECONDS to a larger number of seconds.
The script is intentionally written in a linear, step-by-step style so it can
double as training material and as the literal-included documentation example.
The only local callbacks left in place are the small workflow request/response
adapters and the condition-specific prompt builders passed into
PromptWorkflowAgent(...).
Code#
examples/prompt_framing_study.py# 1"""Canonical live strategy-comparison walkthrough for the umbrella package."""
2
3from __future__ import annotations
4
5import importlib.util
6import os
7from collections.abc import Sequence
8from pathlib import Path
9
10import design_research as dr
11
12# These constants keep the live walkthrough readable: one packaged problem, one
13# study id, stable artifact paths, and the statistical settings used in the
14# pairwise comparisons later on.
15BASELINE_AGENT_ID = "SeededRandomBaselineAgent"
16PROBLEM_ID = "decision_laptop_design_profit_maximization"
17STUDY_ID = "prompt_strategy_comparison_study"
18OUTPUT_DIR = Path("artifacts") / "examples" / STUDY_ID
19SUMMARY_REPORT_NAME = "prompt_strategy_summary.md"
20DEFAULT_REPLICATES_PER_CONDITION = 50
21SIGNIFICANCE_ALPHA = 0.05
22EXACT_PERMUTATION_THRESHOLD = 250_000
23MONTE_CARLO_PERMUTATIONS = 20_000
24PERMUTATION_TEST_SEED = 17
25STRATEGY_ORDER = (BASELINE_AGENT_ID, "neutral_prompt", "profit_focus_prompt")
26MODEL_BACKED_STRATEGY_IDS = ("neutral_prompt", "profit_focus_prompt")
27PRIMARY_METRIC = "predicted_share"
28SECONDARY_METRIC = "expected_demand_units"
29PAIRWISE_COMPARISONS = (
30 ("profit_focus_prompt", "neutral_prompt"),
31 ("neutral_prompt", BASELINE_AGENT_ID),
32 ("profit_focus_prompt", BASELINE_AGENT_ID),
33)
34
35
36def main() -> None:
37 """Run the live strategy-comparison walkthrough with managed llama.cpp."""
38 # Read runtime settings from the environment and apply the example's default
39 # replicate count when the user does not override it.
40 runtime = llama_cpp_runtime_config(default_replicates=DEFAULT_REPLICATES_PER_CONDITION)
41
42 # Load the packaged decision problem and derive the JSON candidate schema the
43 # model-based agents should emit.
44 packaged_problem = dr.problems.get_problem(PROBLEM_ID)
45 candidate_schema = decision_candidate_schema(packaged_problem)
46
47 # Build the recipe-defined study and then materialize its conditions. The
48 # conditions encode one row per strategy/replicate combination.
49 study = _build_study(replicates=int(runtime["replicates"]))
50 conditions = dr.experiments.build_design(study)
51
52 # Start a managed llama.cpp server client for the duration of the study.
53 # The context manager handles startup/shutdown around the live run.
54 with dr.agents.LlamaCppServerLLMClient(
55 model=str(runtime["model_source"]),
56 hf_model_repo_id=runtime["model_repo"],
57 api_model=str(runtime["model_name"]),
58 host=str(runtime["host"]),
59 port=int(runtime["port"]),
60 context_window=int(runtime["context_window"]),
61 startup_timeout_seconds=float(runtime["startup_timeout_seconds"]),
62 request_timeout_seconds=float(runtime["request_timeout_seconds"]),
63 ) as llm_client:
64 # Each `agent_id` in the strategy bundle maps either to a public agent
65 # id resolved directly by experiments or to one explicit binding that
66 # returns a prompt-driven workflow agent.
67 agent_bindings = {
68 # The neutral condition uses the live model but keeps the instruction
69 # framing generic.
70 "neutral_prompt": _prompt_agent_binding(
71 llm_client=llm_client,
72 candidate_schema=candidate_schema,
73 runtime=runtime,
74 instruction=(
75 "Condition: neutral prompt. Choose the best overall candidate using the "
76 "packaged demand and feasibility information."
77 ),
78 ),
79 # The profit-focused condition swaps only the framing instruction so
80 # the study isolates prompt strategy rather than model identity.
81 "profit_focus_prompt": _prompt_agent_binding(
82 llm_client=llm_client,
83 candidate_schema=candidate_schema,
84 runtime=runtime,
85 instruction=(
86 "Condition: profit-focus prompt. Prioritize choices that maximize "
87 "market share proxy and expected demand."
88 ),
89 ),
90 }
91
92 # Execute the full study while the managed llama.cpp client is running.
93 results = dr.experiments.run_study(
94 study,
95 conditions=conditions,
96 agent_bindings=agent_bindings,
97 checkpoint=False,
98 show_progress=False,
99 )
100
101 # Treat this as a live-runtime check, not merely a deterministic-baseline
102 # check: every model-backed prompt strategy must produce usable evidence.
103 successful_results = _require_successful_model_strategies(results)
104
105 # Export the standard analysis tables so the next steps can work from the
106 # same artifacts users would inspect after their own runs.
107 artifact_paths = dr.experiments.export_analysis_tables(
108 study,
109 conditions=conditions,
110 run_results=results,
111 output_dir=OUTPUT_DIR,
112 )
113
114 # Confirm that the event-level export is structurally valid before building
115 # downstream tables from it.
116 validation_report = dr.analysis.validate_experiment_events(artifact_paths["events.csv"])
117
118 # Build one condition-by-metric table for the primary outcome we care about
119 # and another for a secondary business-facing metric, without hand-loading CSVs.
120 primary_metric_rows = dr.analysis.build_condition_metric_table_from_artifacts(
121 artifact_paths["events.csv"],
122 metric=PRIMARY_METRIC,
123 condition_column="agent_id",
124 )
125 demand_metric_rows = dr.analysis.build_condition_metric_table_from_artifacts(
126 artifact_paths["events.csv"],
127 metric=SECONDARY_METRIC,
128 condition_column="agent_id",
129 )
130
131 # Compare the strategy pairs using the analysis package's pairwise
132 # permutation test helper.
133 comparison_report = dr.analysis.compare_condition_pairs_from_artifacts(
134 artifact_paths["events.csv"],
135 metric=PRIMARY_METRIC,
136 condition_column="agent_id",
137 condition_pairs=PAIRWISE_COMPARISONS,
138 alternative="greater",
139 alpha=SIGNIFICANCE_ALPHA,
140 exact_threshold=EXACT_PERMUTATION_THRESHOLD,
141 n_permutations=MONTE_CARLO_PERMUTATIONS,
142 seed=PERMUTATION_TEST_SEED,
143 )
144
145 # Convert the statistical report into rows that the experiments reporting
146 # helpers can render alongside the study summary.
147 significance_rows = comparison_report.to_significance_rows()
148
149 # Write one consolidated markdown report that includes the study summary,
150 # methods scaffold, variable codebook, and the pairwise comparison brief.
151 summary_path = dr.experiments.write_markdown_report(
152 study.output_dir,
153 SUMMARY_REPORT_NAME,
154 "\n\n".join(
155 [
156 dr.experiments.render_markdown_summary(study, results),
157 dr.experiments.render_methods_scaffold(study),
158 dr.experiments.render_codebook(study, conditions),
159 comparison_report.render_brief(),
160 dr.experiments.render_significance_brief(significance_rows),
161 ]
162 ),
163 )
164
165 # Collapse the metric tables to per-strategy means for a concise console
166 # summary after the run finishes.
167 primary_means = condition_means(primary_metric_rows)
168 demand_means = condition_means(demand_metric_rows)
169
170 # Fail loudly if the exported live data is not structurally usable.
171 if validation_report.errors:
172 raise RuntimeError(
173 "Unified event table validation failed:\n- " + "\n- ".join(validation_report.errors)
174 )
175
176 # Print a guided end-of-run summary so the console output doubles as a quick
177 # tour of the artifacts and the headline comparison result.
178 print("Problem:", PROBLEM_ID)
179 print("Study:", study.study_id)
180 print("Live provider:", runtime["provider_name"])
181 print("Live model API name:", runtime["model_name"])
182 print("Model source:", runtime["model_source"])
183 print("Replicates per condition:", runtime["replicates"])
184 print("Conditions:", len(conditions))
185 print("Runs:", len(results), f"({len(successful_results)} success)")
186 print("Condition means:")
187 for strategy_name in STRATEGY_ORDER:
188 print(
189 f" - agent_id={strategy_name}: "
190 f"mean_{PRIMARY_METRIC}={primary_means.get(strategy_name, 0.0):.4f}, "
191 f"mean_{SECONDARY_METRIC}={demand_means.get(strategy_name, 0.0):.0f}"
192 )
193 print(comparison_report.render_brief())
194 print(dr.experiments.render_significance_brief(significance_rows))
195 print("Event rows valid:", validation_report.is_valid, f"(rows={validation_report.n_rows})")
196 print("Summary report:", summary_path)
197 print("Artifacts:", artifact_names(artifact_paths))
198
199
200def _build_study(*, replicates: int) -> object:
201 """Build the live strategy-comparison recipe study."""
202 # The recipe builder captures the study in one config object. The bundle says
203 # which packaged problems and agent strategies participate; the run budget
204 # says how many replicates to execute.
205 return dr.experiments.build_strategy_comparison_study(
206 dr.experiments.StrategyComparisonConfig(
207 study_id=STUDY_ID,
208 title="Prompt Strategy Comparison Study",
209 description=(
210 "Compare a seeded random baseline, a neutral prompt, and a profit-focused "
211 "prompt on a packaged laptop-design decision problem."
212 ),
213 bundle=dr.experiments.BenchmarkBundle(
214 bundle_id="live-strategy-comparison",
215 name="Live Strategy Comparison Bundle",
216 description="Packaged decision problem with three strategy bindings.",
217 problem_ids=(PROBLEM_ID,),
218 agent_specs=STRATEGY_ORDER,
219 ),
220 run_budget=dr.experiments.RunBudget(replicates=replicates, parallelism=1),
221 output_dir=OUTPUT_DIR,
222 )
223 )
224
225
226def _strategy_prompt(problem_packet: object, *, instruction: str) -> str:
227 """Render one complete strategy prompt from the normalized problem packet."""
228 # Compose the final prompt from a few readable pieces instead of one giant
229 # literal string. That makes it easy to see which lines stay fixed across
230 # conditions and which line changes with the strategy framing.
231 return "\n".join(
232 [
233 "You are solving a packaged design-research decision problem.",
234 "Read the problem brief and return exactly one JSON object candidate.",
235 instruction,
236 "",
237 str(getattr(problem_packet, "brief", "")).strip(),
238 "",
239 "Return JSON only with no markdown fences and no extra commentary.",
240 ]
241 )
242
243
244def artifact_names(artifact_paths: dict[str, Path]) -> str:
245 """Return exported artifact filenames in stable sorted order."""
246 return ", ".join(sorted(path.name for path in artifact_paths.values()))
247
248
249def condition_means(rows: list[dict[str, object]]) -> dict[str, float]:
250 """Compute one mean per condition label from normalized rows."""
251 grouped: dict[str, list[float]] = {}
252 for row in rows:
253 grouped.setdefault(str(row["condition"]), []).append(float(row["value"]))
254 return {
255 condition: (sum(values) / len(values) if values else 0.0)
256 for condition, values in grouped.items()
257 }
258
259
260def _require_successful_model_strategies(results: Sequence[object]) -> list[object]:
261 """Require observed successes from every model-backed prompt strategy."""
262 successful_results: list[object] = []
263 successful_strategy_ids: set[str] = set()
264 model_attempts: dict[str, list[str]] = {
265 strategy_id: [] for strategy_id in MODEL_BACKED_STRATEGY_IDS
266 }
267 for result in results:
268 raw_status = getattr(result, "status", None)
269 status = getattr(raw_status, "value", raw_status)
270 run_spec = getattr(result, "run_spec", None)
271 strategy_ref = getattr(run_spec, "agent_spec_ref", None)
272 strategy_id = str(strategy_ref) if strategy_ref is not None else None
273 if strategy_id in model_attempts:
274 error_info = getattr(result, "error_info", None)
275 detail = str(error_info).strip() if error_info else str(status)
276 if detail not in model_attempts[strategy_id]:
277 model_attempts[strategy_id].append(detail)
278 if status != "success":
279 continue
280 successful_results.append(result)
281 if strategy_id is not None:
282 successful_strategy_ids.add(strategy_id)
283
284 missing_strategy_ids = [
285 strategy_id
286 for strategy_id in MODEL_BACKED_STRATEGY_IDS
287 if strategy_id not in successful_strategy_ids
288 ]
289 if missing_strategy_ids:
290 attempt_summary = "; ".join(
291 f"{strategy_id}: {', '.join(model_attempts[strategy_id]) or 'no result'}"
292 for strategy_id in missing_strategy_ids
293 )
294 raise RuntimeError(
295 "The live walkthrough requires at least one successful result from each "
296 "model-backed prompt strategy. Missing successful strategies: "
297 + ", ".join(missing_strategy_ids)
298 + ". Observed attempts: "
299 + attempt_summary
300 )
301 return successful_results
302
303
304def decision_candidate_schema(problem: object) -> dict[str, object]:
305 """Build a JSON schema for discrete decision-factor candidates."""
306 properties: dict[str, object] = {}
307 required: list[str] = []
308 for factor in getattr(problem, "option_factors", ()):
309 levels = tuple(getattr(factor, "levels", ()))
310 key = str(getattr(factor, "key", ""))
311 if not key or not levels:
312 continue
313 properties[key] = {"type": "number", "enum": list(levels)}
314 required.append(key)
315
316 if not required:
317 raise RuntimeError("Expected a packaged decision problem with explicit option factors.")
318
319 return {
320 "type": "object",
321 "properties": properties,
322 "required": required,
323 "additionalProperties": False,
324 }
325
326
327def llama_cpp_runtime_config(*, default_replicates: int) -> dict[str, object]:
328 """Resolve runtime configuration and fail fast on missing live dependencies."""
329 missing_runtime = [
330 module_name
331 for module_name in ("llama_cpp", "fastapi", "uvicorn")
332 if importlib.util.find_spec(module_name) is None
333 ]
334 if missing_runtime:
335 raise RuntimeError(
336 "Install the owning Agents extra before running the live walkthrough: "
337 'python -m pip install "design-research-agents[llama_cpp]==0.6.0". Missing: '
338 + ", ".join(sorted(missing_runtime))
339 )
340
341 model_source = (
342 os.getenv("LLAMA_CPP_MODEL", "Qwen2.5-1.5B-Instruct-Q4_K_M.gguf").strip()
343 or "Qwen2.5-1.5B-Instruct-Q4_K_M.gguf"
344 )
345 model_repo = (
346 os.getenv("LLAMA_CPP_HF_MODEL_REPO_ID", "bartowski/Qwen2.5-1.5B-Instruct-GGUF").strip()
347 or None
348 )
349 if (
350 model_repo
351 and not Path(model_source).expanduser().exists()
352 and importlib.util.find_spec("huggingface_hub") is None
353 ):
354 raise RuntimeError(
355 "Install the owning Agents extra with "
356 'python -m pip install "design-research-agents[llama_cpp]==0.6.0" or point '
357 "LLAMA_CPP_MODEL at a local GGUF file before running the live walkthrough."
358 )
359
360 replicates = int(os.getenv("PROMPT_STUDY_REPLICATES", str(default_replicates)))
361 if replicates < 2:
362 raise RuntimeError("PROMPT_STUDY_REPLICATES must be at least 2.")
363
364 return {
365 "provider_name": "llama-cpp",
366 "model_source": model_source,
367 "model_name": os.getenv("LLAMA_CPP_API_MODEL", "qwen2-1.5b-q4").strip() or "qwen2-1.5b-q4",
368 "model_repo": model_repo,
369 "host": os.getenv("LLAMA_CPP_HOST", "127.0.0.1").strip() or "127.0.0.1",
370 "port": int(os.getenv("LLAMA_CPP_PORT", "8001")),
371 "context_window": int(os.getenv("LLAMA_CPP_CONTEXT_WINDOW", "4096")),
372 # The first startup may include a roughly 1 GB model download. Keep the
373 # wait finite but long enough for an ordinary laptop connection.
374 "startup_timeout_seconds": float(os.getenv("LLAMA_CPP_STARTUP_TIMEOUT_SECONDS", "300")),
375 "request_timeout_seconds": float(os.getenv("LLAMA_CPP_REQUEST_TIMEOUT_SECONDS", "120")),
376 "replicates": replicates,
377 }
378
379
380def _prompt_agent_binding(
381 *,
382 llm_client: object,
383 candidate_schema: dict[str, object],
384 runtime: dict[str, object],
385 instruction: str,
386) -> object:
387 """Build one condition-scoped prompt workflow agent binding."""
388
389 def _binding(_condition: object) -> object:
390 """Return one prompt workflow agent for a concrete experiment condition."""
391 return dr.agents.PromptWorkflowAgent(
392 workflow=dr.agents.build_json_prompt_workflow(
393 llm_client=llm_client,
394 response_schema=candidate_schema,
395 request_metadata={"study_id": STUDY_ID, "problem_id": PROBLEM_ID},
396 default_request_id_prefix=STUDY_ID,
397 fallback_model_name=str(runtime["model_name"]),
398 fallback_provider=str(runtime["provider_name"]),
399 ),
400 prompt_builder=lambda problem_packet, _run_spec, _condition: _strategy_prompt(
401 problem_packet,
402 instruction=instruction,
403 ),
404 )
405
406 return _binding
407
408
409if __name__ == "__main__":
410 main()
When To Go Direct#
Use the umbrella package when you want one import route for the exact tested package family. Install a component directly when you only need one package or want package-specific internals and extras. See Compatibility And Package Status for the tested version combination and API scope.