extropy-run 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- extropy/__init__.py +3 -0
- extropy/cli/__init__.py +23 -0
- extropy/cli/app.py +74 -0
- extropy/cli/commands/__init__.py +29 -0
- extropy/cli/commands/config_cmd.py +210 -0
- extropy/cli/commands/estimate.py +216 -0
- extropy/cli/commands/extend.py +271 -0
- extropy/cli/commands/network.py +287 -0
- extropy/cli/commands/persona.py +344 -0
- extropy/cli/commands/results.py +60 -0
- extropy/cli/commands/sample.py +324 -0
- extropy/cli/commands/scenario.py +256 -0
- extropy/cli/commands/simulate.py +339 -0
- extropy/cli/commands/spec.py +253 -0
- extropy/cli/commands/validate.py +287 -0
- extropy/cli/display.py +233 -0
- extropy/cli/utils.py +317 -0
- extropy/config.py +311 -0
- extropy/core/__init__.py +16 -0
- extropy/core/llm.py +178 -0
- extropy/core/models/__init__.py +200 -0
- extropy/core/models/network.py +188 -0
- extropy/core/models/population.py +532 -0
- extropy/core/models/results.py +125 -0
- extropy/core/models/sampling.py +35 -0
- extropy/core/models/scenario.py +326 -0
- extropy/core/models/simulation.py +393 -0
- extropy/core/models/validation.py +219 -0
- extropy/core/pricing.py +78 -0
- extropy/core/providers/__init__.py +97 -0
- extropy/core/providers/base.py +231 -0
- extropy/core/providers/claude.py +370 -0
- extropy/core/providers/logging.py +69 -0
- extropy/core/providers/openai.py +504 -0
- extropy/core/rate_limiter.py +564 -0
- extropy/core/rate_limits.py +125 -0
- extropy/population/__init__.py +79 -0
- extropy/population/network/__init__.py +93 -0
- extropy/population/network/config.py +179 -0
- extropy/population/network/config_generator.py +566 -0
- extropy/population/network/generator.py +1006 -0
- extropy/population/network/metrics.py +236 -0
- extropy/population/network/similarity.py +238 -0
- extropy/population/persona/__init__.py +48 -0
- extropy/population/persona/config.py +234 -0
- extropy/population/persona/generator.py +823 -0
- extropy/population/persona/renderer.py +409 -0
- extropy/population/persona/stats.py +77 -0
- extropy/population/sampler/__init__.py +57 -0
- extropy/population/sampler/core.py +364 -0
- extropy/population/sampler/distributions.py +354 -0
- extropy/population/sampler/modifiers.py +246 -0
- extropy/population/spec_builder/__init__.py +64 -0
- extropy/population/spec_builder/binder.py +221 -0
- extropy/population/spec_builder/hydrator.py +229 -0
- extropy/population/spec_builder/hydrators/__init__.py +18 -0
- extropy/population/spec_builder/hydrators/conditional.py +448 -0
- extropy/population/spec_builder/hydrators/derived.py +161 -0
- extropy/population/spec_builder/hydrators/independent.py +196 -0
- extropy/population/spec_builder/hydrators/prompts.py +250 -0
- extropy/population/spec_builder/parsers.py +194 -0
- extropy/population/spec_builder/schemas.py +293 -0
- extropy/population/spec_builder/selector.py +320 -0
- extropy/population/spec_builder/sufficiency.py +92 -0
- extropy/population/validator/__init__.py +55 -0
- extropy/population/validator/llm_response.py +646 -0
- extropy/population/validator/semantic.py +250 -0
- extropy/population/validator/spec.py +42 -0
- extropy/population/validator/structural.py +762 -0
- extropy/scenario/__init__.py +100 -0
- extropy/scenario/compiler.py +303 -0
- extropy/scenario/exposure.py +285 -0
- extropy/scenario/interaction.py +293 -0
- extropy/scenario/outcomes.py +141 -0
- extropy/scenario/parser.py +210 -0
- extropy/scenario/validator.py +565 -0
- extropy/simulation/__init__.py +147 -0
- extropy/simulation/aggregation.py +298 -0
- extropy/simulation/engine.py +1334 -0
- extropy/simulation/estimator.py +289 -0
- extropy/simulation/persona.py +381 -0
- extropy/simulation/progress.py +105 -0
- extropy/simulation/propagation.py +344 -0
- extropy/simulation/reasoning.py +916 -0
- extropy/simulation/state.py +1268 -0
- extropy/simulation/stopping.py +297 -0
- extropy/simulation/timeline.py +259 -0
- extropy/utils/__init__.py +72 -0
- extropy/utils/callbacks.py +81 -0
- extropy/utils/distributions.py +227 -0
- extropy/utils/eval_safe.py +273 -0
- extropy/utils/expressions.py +339 -0
- extropy/utils/graphs.py +117 -0
- extropy/utils/paths.py +67 -0
- extropy_run-0.2.2.dist-info/METADATA +200 -0
- extropy_run-0.2.2.dist-info/RECORD +99 -0
- extropy_run-0.2.2.dist-info/WHEEL +4 -0
- extropy_run-0.2.2.dist-info/entry_points.txt +2 -0
- extropy_run-0.2.2.dist-info/licenses/LICENSE +21 -0
extropy/__init__.py
ADDED
extropy/cli/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""CLI package for Extropy.
|
|
2
|
+
|
|
3
|
+
Supports dual-mode output:
|
|
4
|
+
- Human mode (default): Rich formatting with colors, tables, progress bars
|
|
5
|
+
- Machine mode (--json): Structured JSON output for AI coding tools
|
|
6
|
+
|
|
7
|
+
Exit codes:
|
|
8
|
+
0 = Success
|
|
9
|
+
1 = Validation error
|
|
10
|
+
2 = File not found
|
|
11
|
+
3 = Sampling error
|
|
12
|
+
4 = Network error
|
|
13
|
+
5 = Simulation error
|
|
14
|
+
6 = Scenario error
|
|
15
|
+
10 = User cancelled
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .app import app
|
|
19
|
+
|
|
20
|
+
# Import commands to register them with the app
|
|
21
|
+
from . import commands # noqa: F401
|
|
22
|
+
|
|
23
|
+
__all__ = ["app"]
|
extropy/cli/app.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Core CLI app definition and global state."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(
|
|
9
|
+
name="extropy",
|
|
10
|
+
help="Generate population specs for agent-based simulation.",
|
|
11
|
+
no_args_is_help=True,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
# Global state for JSON mode (set by callback)
|
|
17
|
+
_json_mode = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_json_mode() -> bool:
|
|
21
|
+
"""Get current JSON mode state."""
|
|
22
|
+
return _json_mode
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _version_callback(value: bool) -> None:
|
|
26
|
+
if value:
|
|
27
|
+
from .. import __version__
|
|
28
|
+
|
|
29
|
+
print(f"extropy {__version__}")
|
|
30
|
+
raise typer.Exit()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@app.callback()
|
|
34
|
+
def main_callback(
|
|
35
|
+
json_output: Annotated[
|
|
36
|
+
bool,
|
|
37
|
+
typer.Option(
|
|
38
|
+
"--json",
|
|
39
|
+
help="Output machine-readable JSON instead of human-friendly text",
|
|
40
|
+
is_eager=True,
|
|
41
|
+
),
|
|
42
|
+
] = False,
|
|
43
|
+
version: Annotated[
|
|
44
|
+
bool,
|
|
45
|
+
typer.Option(
|
|
46
|
+
"--version",
|
|
47
|
+
help="Show version and exit",
|
|
48
|
+
callback=_version_callback,
|
|
49
|
+
is_eager=True,
|
|
50
|
+
),
|
|
51
|
+
] = False,
|
|
52
|
+
):
|
|
53
|
+
"""Extropy: Population simulation engine for agent-based modeling.
|
|
54
|
+
|
|
55
|
+
Use --json for machine-readable output suitable for scripting and AI tools.
|
|
56
|
+
"""
|
|
57
|
+
global _json_mode
|
|
58
|
+
_json_mode = json_output
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# Import commands to register them with the app
|
|
62
|
+
from .commands import ( # noqa: E402, F401
|
|
63
|
+
validate,
|
|
64
|
+
extend,
|
|
65
|
+
spec,
|
|
66
|
+
sample,
|
|
67
|
+
network,
|
|
68
|
+
persona,
|
|
69
|
+
scenario,
|
|
70
|
+
simulate,
|
|
71
|
+
estimate,
|
|
72
|
+
results,
|
|
73
|
+
config_cmd,
|
|
74
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""CLI commands for Extropy."""
|
|
2
|
+
|
|
3
|
+
from . import (
|
|
4
|
+
validate,
|
|
5
|
+
extend,
|
|
6
|
+
spec,
|
|
7
|
+
sample,
|
|
8
|
+
network,
|
|
9
|
+
persona,
|
|
10
|
+
scenario,
|
|
11
|
+
simulate,
|
|
12
|
+
estimate,
|
|
13
|
+
results,
|
|
14
|
+
config_cmd,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"validate",
|
|
19
|
+
"extend",
|
|
20
|
+
"spec",
|
|
21
|
+
"sample",
|
|
22
|
+
"network",
|
|
23
|
+
"persona",
|
|
24
|
+
"scenario",
|
|
25
|
+
"simulate",
|
|
26
|
+
"estimate",
|
|
27
|
+
"results",
|
|
28
|
+
"config_cmd",
|
|
29
|
+
]
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Config command for viewing and managing extropy configuration."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from ..app import app, console
|
|
6
|
+
from ...config import (
|
|
7
|
+
get_config,
|
|
8
|
+
reset_config,
|
|
9
|
+
CONFIG_FILE,
|
|
10
|
+
get_api_key,
|
|
11
|
+
get_azure_config,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
VALID_KEYS = {
|
|
16
|
+
"pipeline.provider",
|
|
17
|
+
"pipeline.model_simple",
|
|
18
|
+
"pipeline.model_reasoning",
|
|
19
|
+
"pipeline.model_research",
|
|
20
|
+
"simulation.provider",
|
|
21
|
+
"simulation.model",
|
|
22
|
+
"simulation.pivotal_model",
|
|
23
|
+
"simulation.routine_model",
|
|
24
|
+
"simulation.max_concurrent",
|
|
25
|
+
"simulation.rate_tier",
|
|
26
|
+
"simulation.rpm_override",
|
|
27
|
+
"simulation.tpm_override",
|
|
28
|
+
"simulation.api_format",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
INT_FIELDS = {"max_concurrent", "rate_tier", "rpm_override", "tpm_override"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.command("config")
|
|
35
|
+
def config_command(
|
|
36
|
+
action: str = typer.Argument(
|
|
37
|
+
...,
|
|
38
|
+
help="Action: show, set, reset",
|
|
39
|
+
),
|
|
40
|
+
key: str | None = typer.Argument(
|
|
41
|
+
None,
|
|
42
|
+
help="Config key (e.g. pipeline.provider, simulation.model)",
|
|
43
|
+
),
|
|
44
|
+
value: str | None = typer.Argument(
|
|
45
|
+
None,
|
|
46
|
+
help="Value to set",
|
|
47
|
+
),
|
|
48
|
+
):
|
|
49
|
+
"""View or modify extropy configuration.
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
extropy config show
|
|
53
|
+
extropy config set pipeline.provider claude
|
|
54
|
+
extropy config set simulation.provider openai
|
|
55
|
+
extropy config set simulation.model gpt-5-mini
|
|
56
|
+
extropy config reset
|
|
57
|
+
"""
|
|
58
|
+
if action == "show":
|
|
59
|
+
_show_config()
|
|
60
|
+
elif action == "set":
|
|
61
|
+
if not key or value is None:
|
|
62
|
+
console.print("[red]Usage:[/red] extropy config set <key> <value>")
|
|
63
|
+
console.print()
|
|
64
|
+
console.print("Available keys:")
|
|
65
|
+
for k in sorted(VALID_KEYS):
|
|
66
|
+
console.print(f" {k}")
|
|
67
|
+
raise typer.Exit(1)
|
|
68
|
+
_set_config(key, value)
|
|
69
|
+
elif action == "reset":
|
|
70
|
+
_reset_config()
|
|
71
|
+
else:
|
|
72
|
+
console.print(f"[red]Unknown action:[/red] {action}")
|
|
73
|
+
console.print("Valid actions: show, set, reset")
|
|
74
|
+
raise typer.Exit(1)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _show_config():
|
|
78
|
+
"""Display current resolved configuration."""
|
|
79
|
+
config = get_config()
|
|
80
|
+
|
|
81
|
+
console.print()
|
|
82
|
+
console.print("[bold]Extropy Configuration[/bold]")
|
|
83
|
+
console.print("─" * 40)
|
|
84
|
+
|
|
85
|
+
# Pipeline zone
|
|
86
|
+
console.print()
|
|
87
|
+
console.print("[bold cyan]Pipeline[/bold cyan] (spec, extend, persona, scenario)")
|
|
88
|
+
console.print(f" provider = {config.pipeline.provider}")
|
|
89
|
+
console.print(
|
|
90
|
+
f" model_simple = {config.pipeline.model_simple or '[dim](provider default)[/dim]'}"
|
|
91
|
+
)
|
|
92
|
+
console.print(
|
|
93
|
+
f" model_reasoning = {config.pipeline.model_reasoning or '[dim](provider default)[/dim]'}"
|
|
94
|
+
)
|
|
95
|
+
console.print(
|
|
96
|
+
f" model_research = {config.pipeline.model_research or '[dim](provider default)[/dim]'}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Simulation zone
|
|
100
|
+
console.print()
|
|
101
|
+
console.print("[bold cyan]Simulation[/bold cyan] (agent reasoning)")
|
|
102
|
+
console.print(f" provider = {config.simulation.provider}")
|
|
103
|
+
console.print(
|
|
104
|
+
f" model = {config.simulation.model or '[dim](provider default)[/dim]'}"
|
|
105
|
+
)
|
|
106
|
+
console.print(
|
|
107
|
+
f" pivotal_model = {config.simulation.pivotal_model or '[dim](same as model)[/dim]'}"
|
|
108
|
+
)
|
|
109
|
+
console.print(
|
|
110
|
+
f" routine_model = {config.simulation.routine_model or '[dim](provider default)[/dim]'}"
|
|
111
|
+
)
|
|
112
|
+
console.print(
|
|
113
|
+
f" api_format = {config.simulation.api_format or '[dim](auto)[/dim]'}"
|
|
114
|
+
)
|
|
115
|
+
console.print(f" max_concurrent = {config.simulation.max_concurrent}")
|
|
116
|
+
console.print(
|
|
117
|
+
f" rate_tier = {config.simulation.rate_tier or '[dim](tier 1)[/dim]'}"
|
|
118
|
+
)
|
|
119
|
+
if config.simulation.rpm_override:
|
|
120
|
+
console.print(f" rpm_override = {config.simulation.rpm_override}")
|
|
121
|
+
if config.simulation.tpm_override:
|
|
122
|
+
console.print(f" tpm_override = {config.simulation.tpm_override}")
|
|
123
|
+
|
|
124
|
+
# API keys status
|
|
125
|
+
console.print()
|
|
126
|
+
console.print("[bold cyan]API Keys[/bold cyan] (from env vars)")
|
|
127
|
+
_show_key_status("openai", "OPENAI_API_KEY")
|
|
128
|
+
_show_key_status("claude", "ANTHROPIC_API_KEY")
|
|
129
|
+
_show_key_status("azure_openai", "AZURE_OPENAI_API_KEY")
|
|
130
|
+
|
|
131
|
+
# Azure-specific config (show when Azure provider is in use)
|
|
132
|
+
active_providers = {config.pipeline.provider, config.simulation.provider}
|
|
133
|
+
if "azure_openai" in active_providers:
|
|
134
|
+
azure_cfg = get_azure_config("azure_openai")
|
|
135
|
+
console.print()
|
|
136
|
+
console.print("[bold cyan]Azure OpenAI[/bold cyan]")
|
|
137
|
+
console.print(
|
|
138
|
+
f" endpoint = {azure_cfg['azure_endpoint'] or '[dim]not set[/dim]'}"
|
|
139
|
+
)
|
|
140
|
+
console.print(f" api_version = {azure_cfg['api_version']}")
|
|
141
|
+
if azure_cfg["azure_deployment"]:
|
|
142
|
+
console.print(f" deployment = {azure_cfg['azure_deployment']}")
|
|
143
|
+
|
|
144
|
+
# Config file
|
|
145
|
+
console.print()
|
|
146
|
+
if CONFIG_FILE.exists():
|
|
147
|
+
console.print(f"Config file: {CONFIG_FILE}")
|
|
148
|
+
else:
|
|
149
|
+
console.print(f"Config file: [dim]not created yet[/dim] ({CONFIG_FILE})")
|
|
150
|
+
console.print()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _show_key_status(provider: str, env_var_label: str):
|
|
154
|
+
"""Show whether an API key is configured."""
|
|
155
|
+
key = get_api_key(provider)
|
|
156
|
+
if key:
|
|
157
|
+
masked = key[:8] + "..." + key[-4:] if len(key) > 16 else "***"
|
|
158
|
+
console.print(f" {env_var_label}: [green]{masked}[/green]")
|
|
159
|
+
else:
|
|
160
|
+
console.print(f" {env_var_label}: [dim]not set[/dim]")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _set_config(key: str, value: str):
|
|
164
|
+
"""Set a config value and save."""
|
|
165
|
+
if key not in VALID_KEYS:
|
|
166
|
+
console.print(f"[red]Unknown key:[/red] {key}")
|
|
167
|
+
console.print()
|
|
168
|
+
console.print("Available keys:")
|
|
169
|
+
for k in sorted(VALID_KEYS):
|
|
170
|
+
console.print(f" {k}")
|
|
171
|
+
raise typer.Exit(1)
|
|
172
|
+
|
|
173
|
+
# Load current config (or defaults if no file)
|
|
174
|
+
config = get_config()
|
|
175
|
+
|
|
176
|
+
zone, field = key.split(".", 1)
|
|
177
|
+
if zone == "pipeline":
|
|
178
|
+
target = config.pipeline
|
|
179
|
+
elif zone == "simulation":
|
|
180
|
+
target = config.simulation
|
|
181
|
+
else:
|
|
182
|
+
console.print(f"[red]Unknown zone:[/red] {zone}")
|
|
183
|
+
raise typer.Exit(1)
|
|
184
|
+
|
|
185
|
+
# Type coercion
|
|
186
|
+
if field in INT_FIELDS:
|
|
187
|
+
try:
|
|
188
|
+
setattr(target, field, int(value))
|
|
189
|
+
except ValueError:
|
|
190
|
+
console.print(f"[red]Invalid integer value:[/red] {value}")
|
|
191
|
+
raise typer.Exit(1)
|
|
192
|
+
else:
|
|
193
|
+
setattr(target, field, value)
|
|
194
|
+
|
|
195
|
+
config.save()
|
|
196
|
+
reset_config() # Clear cached singleton so next get_config() reloads
|
|
197
|
+
|
|
198
|
+
console.print(f"[green]✓[/green] Set {key} = {value}")
|
|
199
|
+
console.print(f" Saved to {CONFIG_FILE}")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _reset_config():
|
|
203
|
+
"""Reset config to defaults."""
|
|
204
|
+
if CONFIG_FILE.exists():
|
|
205
|
+
CONFIG_FILE.unlink()
|
|
206
|
+
reset_config()
|
|
207
|
+
console.print("[green]✓[/green] Config reset to defaults")
|
|
208
|
+
console.print(f" Removed {CONFIG_FILE}")
|
|
209
|
+
else:
|
|
210
|
+
console.print("Config already at defaults (no config file exists)")
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Estimate command for predicting simulation costs before running."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from ..app import app, console
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@app.command("estimate")
|
|
12
|
+
def estimate_command(
|
|
13
|
+
scenario_file: Path = typer.Argument(..., help="Scenario spec YAML file"),
|
|
14
|
+
model: str = typer.Option(
|
|
15
|
+
"",
|
|
16
|
+
"--model",
|
|
17
|
+
"-m",
|
|
18
|
+
help="LLM model for agent reasoning (empty = use config default)",
|
|
19
|
+
),
|
|
20
|
+
pivotal_model: str = typer.Option(
|
|
21
|
+
"",
|
|
22
|
+
"--pivotal-model",
|
|
23
|
+
help="Model for pivotal/first-pass reasoning (default: same as --model)",
|
|
24
|
+
),
|
|
25
|
+
routine_model: str = typer.Option(
|
|
26
|
+
"",
|
|
27
|
+
"--routine-model",
|
|
28
|
+
help="Cheap model for classification pass (default: provider cheap tier)",
|
|
29
|
+
),
|
|
30
|
+
threshold: int = typer.Option(
|
|
31
|
+
3, "--threshold", "-t", help="Multi-touch threshold for re-reasoning"
|
|
32
|
+
),
|
|
33
|
+
verbose: bool = typer.Option(
|
|
34
|
+
False, "--verbose", "-v", help="Show per-timestep breakdown"
|
|
35
|
+
),
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Estimate simulation cost without running it.
|
|
39
|
+
|
|
40
|
+
Loads the scenario and population files, runs a simplified propagation
|
|
41
|
+
model, and predicts LLM calls, tokens, and USD cost. No API keys required.
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
extropy estimate scenario.yaml
|
|
45
|
+
extropy estimate scenario.yaml --model gpt-5-mini
|
|
46
|
+
extropy estimate scenario.yaml --pivotal-model gpt-5 --routine-model gpt-5-mini -v
|
|
47
|
+
"""
|
|
48
|
+
from ...config import get_config
|
|
49
|
+
from ...core.models import ScenarioSpec, PopulationSpec
|
|
50
|
+
from ...population.network import load_agents_json
|
|
51
|
+
from ...simulation.estimator import estimate_simulation_cost
|
|
52
|
+
|
|
53
|
+
# Validate input file
|
|
54
|
+
if not scenario_file.exists():
|
|
55
|
+
console.print(f"[red]x[/red] Scenario file not found: {scenario_file}")
|
|
56
|
+
raise typer.Exit(1)
|
|
57
|
+
|
|
58
|
+
# Load scenario
|
|
59
|
+
try:
|
|
60
|
+
scenario = ScenarioSpec.from_yaml(scenario_file)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
console.print(f"[red]x[/red] Failed to load scenario: {e}")
|
|
63
|
+
raise typer.Exit(1)
|
|
64
|
+
|
|
65
|
+
# Load population spec
|
|
66
|
+
pop_path = Path(scenario.meta.population_spec)
|
|
67
|
+
if not pop_path.is_absolute():
|
|
68
|
+
pop_path = scenario_file.parent / pop_path
|
|
69
|
+
if not pop_path.exists():
|
|
70
|
+
console.print(f"[red]x[/red] Population spec not found: {pop_path}")
|
|
71
|
+
raise typer.Exit(1)
|
|
72
|
+
population_spec = PopulationSpec.from_yaml(pop_path)
|
|
73
|
+
|
|
74
|
+
# Load agents
|
|
75
|
+
agents_path = Path(scenario.meta.agents_file)
|
|
76
|
+
if not agents_path.is_absolute():
|
|
77
|
+
agents_path = scenario_file.parent / agents_path
|
|
78
|
+
if not agents_path.exists():
|
|
79
|
+
console.print(f"[red]x[/red] Agents file not found: {agents_path}")
|
|
80
|
+
raise typer.Exit(1)
|
|
81
|
+
agents = load_agents_json(agents_path)
|
|
82
|
+
|
|
83
|
+
# Load network
|
|
84
|
+
network_path = Path(scenario.meta.network_file)
|
|
85
|
+
if not network_path.is_absolute():
|
|
86
|
+
network_path = scenario_file.parent / network_path
|
|
87
|
+
if not network_path.exists():
|
|
88
|
+
console.print(f"[red]x[/red] Network file not found: {network_path}")
|
|
89
|
+
raise typer.Exit(1)
|
|
90
|
+
with open(network_path) as f:
|
|
91
|
+
network = json.load(f)
|
|
92
|
+
|
|
93
|
+
# Resolve config
|
|
94
|
+
config = get_config()
|
|
95
|
+
provider = config.simulation.provider
|
|
96
|
+
|
|
97
|
+
eff_model = model or config.simulation.model
|
|
98
|
+
eff_pivotal = pivotal_model or config.simulation.pivotal_model or eff_model
|
|
99
|
+
eff_routine = routine_model or config.simulation.routine_model
|
|
100
|
+
|
|
101
|
+
# Run estimation
|
|
102
|
+
est = estimate_simulation_cost(
|
|
103
|
+
scenario=scenario,
|
|
104
|
+
population_spec=population_spec,
|
|
105
|
+
agents=agents,
|
|
106
|
+
network=network,
|
|
107
|
+
provider=provider,
|
|
108
|
+
pivotal_model=eff_pivotal,
|
|
109
|
+
routine_model=eff_routine,
|
|
110
|
+
multi_touch_threshold=threshold,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Display results
|
|
114
|
+
console.print()
|
|
115
|
+
console.print("[bold]Simulation Cost Estimate[/bold]")
|
|
116
|
+
console.print()
|
|
117
|
+
|
|
118
|
+
early_stop = (
|
|
119
|
+
f" (early stop at ~{est.per_timestep[-1]['exposure_rate']:.0%} exposure)"
|
|
120
|
+
if est.effective_timesteps < est.max_timesteps and est.per_timestep
|
|
121
|
+
else ""
|
|
122
|
+
)
|
|
123
|
+
console.print(
|
|
124
|
+
f"Population: {est.population_size} agents | "
|
|
125
|
+
f"Avg degree: {est.avg_degree:.1f} | "
|
|
126
|
+
f"Max timesteps: {est.max_timesteps}"
|
|
127
|
+
)
|
|
128
|
+
console.print(f"Effective timesteps: ~{est.effective_timesteps}{early_stop}")
|
|
129
|
+
console.print()
|
|
130
|
+
|
|
131
|
+
# Models section
|
|
132
|
+
console.print("[bold]Models[/bold]")
|
|
133
|
+
_print_model_line(
|
|
134
|
+
console, "Pass 1 (pivotal)", est.pivotal_model, est.pivotal_pricing
|
|
135
|
+
)
|
|
136
|
+
_print_model_line(
|
|
137
|
+
console, "Pass 2 (routine)", est.routine_model, est.routine_pricing
|
|
138
|
+
)
|
|
139
|
+
console.print()
|
|
140
|
+
|
|
141
|
+
# Calls table
|
|
142
|
+
console.print("[bold]Estimated LLM Calls[/bold]")
|
|
143
|
+
console.print(
|
|
144
|
+
f" {'':16s} {'Calls':>8s} {'Input Tok':>12s} {'Output Tok':>12s}"
|
|
145
|
+
)
|
|
146
|
+
console.print(
|
|
147
|
+
f" {'Pass 1':16s} {est.pass1_calls:>8,} "
|
|
148
|
+
f"{'~' + f'{est.pass1_input_tokens:,}':>12s} "
|
|
149
|
+
f"{'~' + f'{est.pass1_output_tokens:,}':>12s}"
|
|
150
|
+
)
|
|
151
|
+
console.print(
|
|
152
|
+
f" {'Pass 2':16s} {est.pass2_calls:>8,} "
|
|
153
|
+
f"{'~' + f'{est.pass2_input_tokens:,}':>12s} "
|
|
154
|
+
f"{'~' + f'{est.pass2_output_tokens:,}':>12s}"
|
|
155
|
+
)
|
|
156
|
+
total_input = est.pass1_input_tokens + est.pass2_input_tokens
|
|
157
|
+
total_output = est.pass1_output_tokens + est.pass2_output_tokens
|
|
158
|
+
console.print(
|
|
159
|
+
f" {'Total':16s} {est.pass1_calls + est.pass2_calls:>8,} "
|
|
160
|
+
f"{'~' + f'{total_input:,}':>12s} "
|
|
161
|
+
f"{'~' + f'{total_output:,}':>12s}"
|
|
162
|
+
)
|
|
163
|
+
console.print()
|
|
164
|
+
|
|
165
|
+
# Cost section
|
|
166
|
+
console.print("[bold]Estimated Cost[/bold]")
|
|
167
|
+
if est.pass1_cost is not None:
|
|
168
|
+
console.print(f" Pass 1: ${est.pass1_cost:.2f}")
|
|
169
|
+
else:
|
|
170
|
+
console.print(
|
|
171
|
+
f" Pass 1: [dim]pricing not available for {est.pivotal_model}[/dim]"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if est.pass2_cost is not None:
|
|
175
|
+
console.print(f" Pass 2: ${est.pass2_cost:.2f}")
|
|
176
|
+
else:
|
|
177
|
+
console.print(
|
|
178
|
+
f" Pass 2: [dim]pricing not available for {est.routine_model}[/dim]"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if est.total_cost is not None:
|
|
182
|
+
console.print(f" [bold]Total: ${est.total_cost:.2f}[/bold]")
|
|
183
|
+
console.print()
|
|
184
|
+
|
|
185
|
+
console.print(
|
|
186
|
+
"[dim]Estimates are approximate. Actual costs vary with prompt length "
|
|
187
|
+
"and simulation dynamics.[/dim]"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Verbose per-timestep breakdown
|
|
191
|
+
if verbose and est.per_timestep:
|
|
192
|
+
console.print()
|
|
193
|
+
console.print("[bold]Per-Timestep Breakdown[/bold]")
|
|
194
|
+
console.print(
|
|
195
|
+
f" {'Step':>6s} {'Exposure':>9s} {'New Exp':>8s} {'Reasoning':>10s}"
|
|
196
|
+
)
|
|
197
|
+
for row in est.per_timestep:
|
|
198
|
+
if row["new_exposures"] > 0 or row["reasoning_calls"] > 0:
|
|
199
|
+
console.print(
|
|
200
|
+
f" {row['timestep']:>6d} "
|
|
201
|
+
f"{row['exposure_rate']:>8.1%} "
|
|
202
|
+
f"{row['new_exposures']:>8d} "
|
|
203
|
+
f"{row['reasoning_calls']:>10d}"
|
|
204
|
+
)
|
|
205
|
+
console.print()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _print_model_line(console, label: str, model: str, pricing):
|
|
209
|
+
"""Print a model info line with optional pricing."""
|
|
210
|
+
if pricing:
|
|
211
|
+
console.print(
|
|
212
|
+
f" {label}: {model} "
|
|
213
|
+
f"(${pricing.input_per_mtok:.2f} / ${pricing.output_per_mtok:.2f} per MTok in/out)"
|
|
214
|
+
)
|
|
215
|
+
else:
|
|
216
|
+
console.print(f" {label}: {model} [dim](pricing not available)[/dim]")
|