|
24 | 24 | EviiResult, |
25 | 25 | EvpiResult, |
26 | 26 | ImportResult, |
| 27 | + InputDistributionResult, |
27 | 28 | KeyValue, |
28 | 29 | McdaBuildResult, |
29 | 30 | McdaSpec, |
@@ -1031,6 +1032,95 @@ def build_control_panel( |
1031 | 1032 | ) |
1032 | 1033 |
|
1033 | 1034 |
|
| 1035 | +@mcp.tool( |
| 1036 | + description=( |
| 1037 | + "ModelChoice: Assign an UNCERTAINTY to a tree input — put a ModelRisk " |
| 1038 | + "`Vose*` distribution on a branch's cash flow or probability, the way the " |
| 1039 | + "ModelChoice UI lets you type a distribution into an input cell. Pass the " |
| 1040 | + "node id, the branch/option label, the distribution formula (e.g. " |
| 1041 | + "'VoseNormal(100,20)', 'VosePERT(80,100,150)'), and kind='value' (cash " |
| 1042 | + "flow) or 'probability'. The distribution is stored as the cell's " |
| 1043 | + "user-formula and re-rendered, so it survives edits. This is the " |
| 1044 | + "decision-tree half of a Monte Carlo: once inputs are distributions, run " |
| 1045 | + "the simulation with modelrisk-mcp (it samples these cells and collects " |
| 1046 | + "the tree's output distribution). Requires the add-in loaded with a tree " |
| 1047 | + "rendered. Branch values / probabilities only (not terminal payoffs)." |
| 1048 | + ) |
| 1049 | +) |
| 1050 | +def set_input_distribution( |
| 1051 | + node_id: Annotated[str, Field(description="Node whose branch/option carries the input.")], |
| 1052 | + outcome: Annotated[ |
| 1053 | + str, Field(description="The branch/option label to put the distribution on.") |
| 1054 | + ], |
| 1055 | + distribution: Annotated[ |
| 1056 | + str, |
| 1057 | + Field( |
| 1058 | + description=( |
| 1059 | + "The ModelRisk distribution formula, e.g. 'VoseNormal(100,20)' or " |
| 1060 | + "'=VosePERT(80,100,150)'. A leading '=' is optional." |
| 1061 | + ) |
| 1062 | + ), |
| 1063 | + ], |
| 1064 | + kind: Annotated[ |
| 1065 | + str, Field(description="'value' (branch cash flow) or 'probability'.") |
| 1066 | + ] = "value", |
| 1067 | + tree_name: Annotated[ |
| 1068 | + str | None, Field(description="Tree sheet name. Omit for the first tree.") |
| 1069 | + ] = None, |
| 1070 | + workbook_name: str | None = None, |
| 1071 | +) -> InputDistributionResult: |
| 1072 | + k = kind.lower() |
| 1073 | + if k not in ("value", "probability"): |
| 1074 | + raise ValueError(f"kind must be 'value' or 'probability', got {kind!r}.") |
| 1075 | + |
| 1076 | + bridge = get_bridge() |
| 1077 | + trees = bridge.list_trees(workbook_name) |
| 1078 | + if tree_name is None: |
| 1079 | + tree_name = next(iter(trees)) |
| 1080 | + if tree_name not in trees: |
| 1081 | + raise TreeParseError(f"no tree {tree_name!r}; available: {', '.join(trees)}.") |
| 1082 | + |
| 1083 | + tree = parse_model(trees[tree_name]) |
| 1084 | + node = tree.nodes.get(node_id) |
| 1085 | + if node is None: |
| 1086 | + raise ValueError(f"node {node_id!r} not found in {tree_name!r}.") |
| 1087 | + branch = next((b for b in node.branches if b.name == outcome), None) |
| 1088 | + if branch is None: |
| 1089 | + names = ", ".join(b.name for b in node.branches) or "(none)" |
| 1090 | + raise ValueError( |
| 1091 | + f"node {node_id!r} ({node.name!r}) has no branch {outcome!r}. " |
| 1092 | + f"Branches: {names}." |
| 1093 | + ) |
| 1094 | + if k == "probability" and node.kind != "chance": |
| 1095 | + raise ValueError( |
| 1096 | + f"kind='probability' is only valid on a chance node; {node_id!r} is a " |
| 1097 | + f"{node.kind} node. Use kind='value' for a decision option's cash flow." |
| 1098 | + ) |
| 1099 | + |
| 1100 | + prefix = "MC_BV_" if k == "value" else "MC_BP_" |
| 1101 | + named_range = f"{prefix}{node_id}_{branch.child_id}" |
| 1102 | + formula = distribution.strip() |
| 1103 | + if not formula.startswith("="): |
| 1104 | + formula = "=" + formula |
| 1105 | + |
| 1106 | + sheet = bridge.set_input_formula(named_range, formula, tree_name, workbook_name) |
| 1107 | + |
| 1108 | + return InputDistributionResult( |
| 1109 | + tree=sheet, |
| 1110 | + node_id=node_id, |
| 1111 | + outcome=outcome, |
| 1112 | + kind=k, |
| 1113 | + named_range=named_range, |
| 1114 | + formula=formula, |
| 1115 | + note=( |
| 1116 | + f"Put {formula} on {node.name!r} → {outcome!r} ({named_range}). It's now " |
| 1117 | + "an uncertain input. Run the Monte Carlo with modelrisk-mcp's " |
| 1118 | + "run_simulation (track the root EV cell as the output) to get the " |
| 1119 | + "tree's outcome distribution." |
| 1120 | + ), |
| 1121 | + ) |
| 1122 | + |
| 1123 | + |
1034 | 1124 | @mcp.tool( |
1035 | 1125 | description=( |
1036 | 1126 | "ModelChoice: Run a decision-analysis on the active tree and report the " |
@@ -1429,5 +1519,6 @@ def read_sheet( |
1429 | 1519 | "run_sensitivity", |
1430 | 1520 | "run_utility", |
1431 | 1521 | "set_bridge_for_testing", |
| 1522 | + "set_input_distribution", |
1432 | 1523 | "verify_rollback", |
1433 | 1524 | ] |
0 commit comments