explicator 0.1.0__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.
- explicator/__init__.py +169 -0
- explicator/adapters/__init__.py +1 -0
- explicator/adapters/cli/__init__.py +1 -0
- explicator/adapters/cli/main.py +158 -0
- explicator/adapters/data/__init__.py +1 -0
- explicator/adapters/data/in_memory.py +134 -0
- explicator/adapters/mcp_server/__init__.py +1 -0
- explicator/adapters/mcp_server/__main__.py +29 -0
- explicator/adapters/mcp_server/server.py +308 -0
- explicator/ai/__init__.py +1 -0
- explicator/ai/dispatcher.py +71 -0
- explicator/ai/providers/__init__.py +1 -0
- explicator/ai/providers/azure_openai.py +111 -0
- explicator/ai/providers/base.py +60 -0
- explicator/ai/providers/claude.py +114 -0
- explicator/ai/tools/__init__.py +1 -0
- explicator/ai/tools/definitions.py +124 -0
- explicator/application/__init__.py +1 -0
- explicator/application/service.py +154 -0
- explicator/config.py +72 -0
- explicator/domain/__init__.py +1 -0
- explicator/domain/models.py +160 -0
- explicator/domain/ports.py +43 -0
- explicator-0.1.0.dist-info/METADATA +21 -0
- explicator-0.1.0.dist-info/RECORD +27 -0
- explicator-0.1.0.dist-info/WHEEL +4 -0
- explicator-0.1.0.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Tool definitions in OpenAI function-calling JSON schema format.
|
|
2
|
+
|
|
3
|
+
This module is the single source of truth for all tool schemas.
|
|
4
|
+
All AI providers (Claude, Azure OpenAI, etc.) consume these definitions
|
|
5
|
+
and translate to their own wire format internally.
|
|
6
|
+
|
|
7
|
+
No provider-specific code lives here.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
TOOL_DEFINITIONS: list[dict] = [
|
|
11
|
+
{
|
|
12
|
+
"type": "function",
|
|
13
|
+
"function": {
|
|
14
|
+
"name": "run_scenario",
|
|
15
|
+
"description": (
|
|
16
|
+
"Run a named scenario through the portfolio model, optionally applying "
|
|
17
|
+
"additional input overrides for this run only. Returns the full set of "
|
|
18
|
+
"model outputs including portfolio metrics."
|
|
19
|
+
),
|
|
20
|
+
"parameters": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"properties": {
|
|
23
|
+
"name": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "The name of the scenario to run.",
|
|
26
|
+
},
|
|
27
|
+
"overrides": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"description": (
|
|
30
|
+
"Optional map of input field names to numeric values to "
|
|
31
|
+
"override for this run only. These do not persist."
|
|
32
|
+
),
|
|
33
|
+
"additionalProperties": {"type": "number"},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
"required": ["name"],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"type": "function",
|
|
42
|
+
"function": {
|
|
43
|
+
"name": "override_input",
|
|
44
|
+
"description": (
|
|
45
|
+
"Apply a persistent session-level override to a specific model input. "
|
|
46
|
+
"This override will be applied to all subsequent scenario runs until "
|
|
47
|
+
"reset_overrides is called."
|
|
48
|
+
),
|
|
49
|
+
"parameters": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"properties": {
|
|
52
|
+
"source": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "The input source group, e.g. 'rates'.",
|
|
55
|
+
},
|
|
56
|
+
"field": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"description": "The field name within that source group.",
|
|
59
|
+
},
|
|
60
|
+
"value": {
|
|
61
|
+
"type": "number",
|
|
62
|
+
"description": "The numeric value to apply.",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
"required": ["source", "field", "value"],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"type": "function",
|
|
71
|
+
"function": {
|
|
72
|
+
"name": "reset_overrides",
|
|
73
|
+
"description": (
|
|
74
|
+
"Clear all active session-level input overrides, restoring all model "
|
|
75
|
+
"inputs to their configured defaults."
|
|
76
|
+
),
|
|
77
|
+
"parameters": {"type": "object", "properties": {}, "required": []},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"type": "function",
|
|
82
|
+
"function": {
|
|
83
|
+
"name": "compare_scenarios",
|
|
84
|
+
"description": (
|
|
85
|
+
"Run two named scenarios and return a side-by-side comparison "
|
|
86
|
+
"of their outputs, including absolute and percentage differences. "
|
|
87
|
+
"Optionally restrict the comparison to specific output metrics."
|
|
88
|
+
),
|
|
89
|
+
"parameters": {
|
|
90
|
+
"type": "object",
|
|
91
|
+
"properties": {
|
|
92
|
+
"scenario_a": {
|
|
93
|
+
"type": "string",
|
|
94
|
+
"description": "Name of the first scenario.",
|
|
95
|
+
},
|
|
96
|
+
"scenario_b": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"description": "Name of the second scenario.",
|
|
99
|
+
},
|
|
100
|
+
"metrics": {
|
|
101
|
+
"type": "array",
|
|
102
|
+
"items": {"type": "string"},
|
|
103
|
+
"description": (
|
|
104
|
+
"Optional list of output field names to compare. "
|
|
105
|
+
"If omitted, all shared outputs are compared."
|
|
106
|
+
),
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
"required": ["scenario_a", "scenario_b"],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"type": "function",
|
|
115
|
+
"function": {
|
|
116
|
+
"name": "get_available_scenarios",
|
|
117
|
+
"description": (
|
|
118
|
+
"List all configured scenarios with their names, descriptions, "
|
|
119
|
+
"and stress rationale."
|
|
120
|
+
),
|
|
121
|
+
"parameters": {"type": "object", "properties": {}, "required": []},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Application service layer for Explicator."""
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Application service layer — the single entry point for all business logic.
|
|
2
|
+
|
|
3
|
+
All adapters (CLI, MCP server, AI orchestrator) call into this service.
|
|
4
|
+
No business logic lives in adapters. No framework code lives here.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from explicator.domain.models import (
|
|
12
|
+
ModelSchema,
|
|
13
|
+
Override,
|
|
14
|
+
ScenarioComparison,
|
|
15
|
+
ScenarioDefinition,
|
|
16
|
+
ScenarioResult,
|
|
17
|
+
)
|
|
18
|
+
from explicator.domain.ports import ModelRepository, ScenarioRunner
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ModelService:
|
|
22
|
+
"""
|
|
23
|
+
Framework-agnostic application service layer.
|
|
24
|
+
|
|
25
|
+
Owns session state (active overrides, cached results). Delegates
|
|
26
|
+
scenario execution to the ScenarioRunner port and model metadata
|
|
27
|
+
to the ModelRepository port.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, runner: ScenarioRunner, repository: ModelRepository) -> None:
|
|
31
|
+
"""Initialise the service with a runner and repository."""
|
|
32
|
+
self._runner = runner
|
|
33
|
+
self._repository = repository
|
|
34
|
+
self._overrides: list[Override] = []
|
|
35
|
+
self._last_results: dict[str, ScenarioResult] = {}
|
|
36
|
+
|
|
37
|
+
# ------------------------------------------------------------------
|
|
38
|
+
# Queries
|
|
39
|
+
# ------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
def get_available_scenarios(self) -> list[ScenarioDefinition]:
|
|
42
|
+
"""Return all configured scenarios with their descriptions."""
|
|
43
|
+
return self._repository.get_scenarios()
|
|
44
|
+
|
|
45
|
+
def get_current_results(self) -> dict[str, ScenarioResult]:
|
|
46
|
+
"""Return the most recent result for each scenario run this session."""
|
|
47
|
+
return dict(self._last_results)
|
|
48
|
+
|
|
49
|
+
def get_model_schema(self) -> ModelSchema:
|
|
50
|
+
"""Return the full structured description of model inputs and outputs."""
|
|
51
|
+
return self._repository.get_schema()
|
|
52
|
+
|
|
53
|
+
def get_active_overrides(self) -> list[Override]:
|
|
54
|
+
"""Return all currently active session-level input overrides."""
|
|
55
|
+
return list(self._overrides)
|
|
56
|
+
|
|
57
|
+
# ------------------------------------------------------------------
|
|
58
|
+
# Commands
|
|
59
|
+
# ------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
def run_scenario(
|
|
62
|
+
self,
|
|
63
|
+
name: str,
|
|
64
|
+
overrides: dict[str, Any] | None = None,
|
|
65
|
+
) -> ScenarioResult:
|
|
66
|
+
"""
|
|
67
|
+
Run a named scenario, merging active overrides with any call-specific overrides.
|
|
68
|
+
|
|
69
|
+
Precedence (highest wins): call-specific overrides > session overrides >
|
|
70
|
+
scenario definition overrides > model base inputs.
|
|
71
|
+
"""
|
|
72
|
+
scenarios = {s.name: s for s in self._repository.get_scenarios()}
|
|
73
|
+
if name not in scenarios:
|
|
74
|
+
available = ", ".join(scenarios.keys())
|
|
75
|
+
raise ValueError(f"Unknown scenario '{name}'. Available: {available}")
|
|
76
|
+
|
|
77
|
+
scenario = scenarios[name]
|
|
78
|
+
|
|
79
|
+
combined: dict[str, Any] = {}
|
|
80
|
+
for ov in self._overrides:
|
|
81
|
+
combined[ov.field] = ov.value
|
|
82
|
+
if overrides:
|
|
83
|
+
combined.update(overrides)
|
|
84
|
+
|
|
85
|
+
result = self._runner.run(scenario, extra_overrides=combined)
|
|
86
|
+
self._last_results[name] = result
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
def override_input(self, source: str, field: str, value: float) -> str:
|
|
90
|
+
"""Apply a session-level override to a specific input field.
|
|
91
|
+
|
|
92
|
+
Replaces any existing override for the same field.
|
|
93
|
+
"""
|
|
94
|
+
self._overrides = [o for o in self._overrides if o.field != field]
|
|
95
|
+
self._overrides.append(Override(source=source, field=field, value=value))
|
|
96
|
+
return f"Override applied: {source}.{field} = {value}"
|
|
97
|
+
|
|
98
|
+
def reset_overrides(self) -> str:
|
|
99
|
+
"""Clear all active session-level input overrides."""
|
|
100
|
+
count = len(self._overrides)
|
|
101
|
+
self._overrides = []
|
|
102
|
+
return f"Cleared {count} override(s). All inputs restored to model defaults."
|
|
103
|
+
|
|
104
|
+
def compare_scenarios(
|
|
105
|
+
self,
|
|
106
|
+
scenario_a: str,
|
|
107
|
+
scenario_b: str,
|
|
108
|
+
metrics: list[str] | None = None,
|
|
109
|
+
) -> ScenarioComparison:
|
|
110
|
+
"""Run two scenarios and return a structured side-by-side comparison.
|
|
111
|
+
|
|
112
|
+
If metrics is None, all output fields present in both results are compared.
|
|
113
|
+
"""
|
|
114
|
+
result_a = self.run_scenario(scenario_a)
|
|
115
|
+
result_b = self.run_scenario(scenario_b)
|
|
116
|
+
|
|
117
|
+
all_outputs = set(result_a.outputs.keys()) | set(result_b.outputs.keys())
|
|
118
|
+
compare_metrics = metrics if metrics else sorted(all_outputs)
|
|
119
|
+
|
|
120
|
+
differences: dict[str, dict] = {}
|
|
121
|
+
for metric in compare_metrics:
|
|
122
|
+
val_a = result_a.outputs.get(metric)
|
|
123
|
+
val_b = result_b.outputs.get(metric)
|
|
124
|
+
if val_a is not None and val_b is not None:
|
|
125
|
+
try:
|
|
126
|
+
delta = float(val_b) - float(val_a)
|
|
127
|
+
pct = (delta / float(val_a) * 100) if val_a != 0 else None
|
|
128
|
+
differences[metric] = {
|
|
129
|
+
"a": val_a,
|
|
130
|
+
"b": val_b,
|
|
131
|
+
"delta": round(delta, 6),
|
|
132
|
+
"pct_change": round(pct, 2) if pct is not None else None,
|
|
133
|
+
}
|
|
134
|
+
except (TypeError, ValueError):
|
|
135
|
+
differences[metric] = {
|
|
136
|
+
"a": val_a,
|
|
137
|
+
"b": val_b,
|
|
138
|
+
"delta": None,
|
|
139
|
+
"pct_change": None,
|
|
140
|
+
}
|
|
141
|
+
else:
|
|
142
|
+
differences[metric] = {
|
|
143
|
+
"a": val_a,
|
|
144
|
+
"b": val_b,
|
|
145
|
+
"delta": None,
|
|
146
|
+
"pct_change": None,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return ScenarioComparison(
|
|
150
|
+
scenario_a=result_a,
|
|
151
|
+
scenario_b=result_b,
|
|
152
|
+
metrics=compare_metrics,
|
|
153
|
+
differences=differences,
|
|
154
|
+
)
|
explicator/config.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Configuration loading for Explicator.
|
|
2
|
+
|
|
3
|
+
Reads from environment variables with optional .env file support.
|
|
4
|
+
Follows the existing application pattern — no new config mechanisms.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from explicator.ai.providers.base import AIProvider
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Config:
|
|
19
|
+
"""Runtime configuration loaded from environment variables."""
|
|
20
|
+
|
|
21
|
+
ai_provider: str = "claude" # "claude" | "azure_openai"
|
|
22
|
+
claude_api_key: str | None = None
|
|
23
|
+
claude_model: str = "claude-sonnet-4-6"
|
|
24
|
+
azure_api_key: str | None = None
|
|
25
|
+
azure_endpoint: str | None = None
|
|
26
|
+
azure_deployment: str | None = None
|
|
27
|
+
azure_api_version: str = "2024-02-01"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load_config() -> Config:
|
|
31
|
+
"""Load configuration from environment variables (and optional .env file)."""
|
|
32
|
+
try:
|
|
33
|
+
from dotenv import load_dotenv
|
|
34
|
+
|
|
35
|
+
load_dotenv()
|
|
36
|
+
except ImportError:
|
|
37
|
+
pass # python-dotenv is optional
|
|
38
|
+
|
|
39
|
+
return Config(
|
|
40
|
+
ai_provider=os.getenv("AI_PROVIDER", "claude"),
|
|
41
|
+
claude_api_key=os.getenv("ANTHROPIC_API_KEY"),
|
|
42
|
+
claude_model=os.getenv("CLAUDE_MODEL", "claude-sonnet-4-6"),
|
|
43
|
+
azure_api_key=os.getenv("AZURE_OPENAI_API_KEY"),
|
|
44
|
+
azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
|
|
45
|
+
azure_deployment=os.getenv("AZURE_OPENAI_DEPLOYMENT"),
|
|
46
|
+
azure_api_version=os.getenv("AZURE_OPENAI_API_VERSION", "2024-02-01"),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_provider(config: Config | None = None) -> AIProvider:
|
|
51
|
+
"""Instantiate the AI provider selected by configuration."""
|
|
52
|
+
if config is None:
|
|
53
|
+
config = load_config()
|
|
54
|
+
|
|
55
|
+
if config.ai_provider == "claude":
|
|
56
|
+
from explicator.ai.providers.claude import ClaudeProvider
|
|
57
|
+
|
|
58
|
+
return ClaudeProvider(api_key=config.claude_api_key, model=config.claude_model)
|
|
59
|
+
|
|
60
|
+
if config.ai_provider == "azure_openai":
|
|
61
|
+
from explicator.ai.providers.azure_openai import AzureOpenAIProvider
|
|
62
|
+
|
|
63
|
+
return AzureOpenAIProvider(
|
|
64
|
+
api_key=config.azure_api_key,
|
|
65
|
+
azure_endpoint=config.azure_endpoint,
|
|
66
|
+
deployment_name=config.azure_deployment,
|
|
67
|
+
api_version=config.azure_api_version,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"Unknown AI_PROVIDER '{config.ai_provider}'. Supported: claude, azure_openai"
|
|
72
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core domain models and port interfaces for Explicator."""
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Core domain model — data structures shared across the entire application.
|
|
2
|
+
|
|
3
|
+
These are plain dataclasses with no framework dependencies. All adapters
|
|
4
|
+
(CLI, MCP server, AI providers) work with these types.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class InputField:
|
|
16
|
+
"""A single model input variable."""
|
|
17
|
+
|
|
18
|
+
name: str
|
|
19
|
+
source: str # logical grouping, e.g. "rates", "credit", "equity"
|
|
20
|
+
description: str
|
|
21
|
+
units: str
|
|
22
|
+
typical_min: float
|
|
23
|
+
typical_max: float
|
|
24
|
+
current_value: float | None = None
|
|
25
|
+
|
|
26
|
+
def to_dict(self) -> dict:
|
|
27
|
+
"""Serialise to a JSON-compatible dict."""
|
|
28
|
+
return {
|
|
29
|
+
"name": self.name,
|
|
30
|
+
"source": self.source,
|
|
31
|
+
"description": self.description,
|
|
32
|
+
"units": self.units,
|
|
33
|
+
"typical_min": self.typical_min,
|
|
34
|
+
"typical_max": self.typical_max,
|
|
35
|
+
"current_value": self.current_value,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class OutputField:
|
|
41
|
+
"""A single model output metric."""
|
|
42
|
+
|
|
43
|
+
name: str
|
|
44
|
+
description: str
|
|
45
|
+
units: str
|
|
46
|
+
interpretation: str
|
|
47
|
+
good_threshold: float | None = None # value below which result is concerning
|
|
48
|
+
bad_threshold: float | None = None # value below which result is critical
|
|
49
|
+
|
|
50
|
+
def to_dict(self) -> dict:
|
|
51
|
+
"""Serialise to a JSON-compatible dict."""
|
|
52
|
+
return {
|
|
53
|
+
"name": self.name,
|
|
54
|
+
"description": self.description,
|
|
55
|
+
"units": self.units,
|
|
56
|
+
"interpretation": self.interpretation,
|
|
57
|
+
"good_threshold": self.good_threshold,
|
|
58
|
+
"bad_threshold": self.bad_threshold,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class ModelSchema:
|
|
64
|
+
"""Full description of the model's inputs, outputs, assumptions, and caveats."""
|
|
65
|
+
|
|
66
|
+
name: str
|
|
67
|
+
description: str
|
|
68
|
+
inputs: list[InputField]
|
|
69
|
+
outputs: list[OutputField]
|
|
70
|
+
assumptions: list[str]
|
|
71
|
+
caveats: list[str]
|
|
72
|
+
|
|
73
|
+
def to_dict(self) -> dict:
|
|
74
|
+
"""Serialise to a JSON-compatible dict."""
|
|
75
|
+
return {
|
|
76
|
+
"name": self.name,
|
|
77
|
+
"description": self.description,
|
|
78
|
+
"inputs": [i.to_dict() for i in self.inputs],
|
|
79
|
+
"outputs": [o.to_dict() for o in self.outputs],
|
|
80
|
+
"assumptions": self.assumptions,
|
|
81
|
+
"caveats": self.caveats,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class ScenarioDefinition:
|
|
87
|
+
"""A named scenario with its baseline overrides and stress description."""
|
|
88
|
+
|
|
89
|
+
name: str
|
|
90
|
+
description: str
|
|
91
|
+
stress_rationale: str
|
|
92
|
+
overrides: dict[str, Any] = field(default_factory=dict)
|
|
93
|
+
|
|
94
|
+
def to_dict(self) -> dict:
|
|
95
|
+
"""Serialise to a JSON-compatible dict."""
|
|
96
|
+
return {
|
|
97
|
+
"name": self.name,
|
|
98
|
+
"description": self.description,
|
|
99
|
+
"stress_rationale": self.stress_rationale,
|
|
100
|
+
"overrides": self.overrides,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class ScenarioResult:
|
|
106
|
+
"""The result of running a scenario through the model."""
|
|
107
|
+
|
|
108
|
+
scenario_name: str
|
|
109
|
+
inputs_used: dict[str, Any]
|
|
110
|
+
outputs: dict[str, Any]
|
|
111
|
+
overrides_applied: dict[str, Any]
|
|
112
|
+
run_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
|
|
113
|
+
|
|
114
|
+
def to_dict(self) -> dict:
|
|
115
|
+
"""Serialise to a JSON-compatible dict."""
|
|
116
|
+
return {
|
|
117
|
+
"scenario_name": self.scenario_name,
|
|
118
|
+
"inputs_used": self.inputs_used,
|
|
119
|
+
"outputs": self.outputs,
|
|
120
|
+
"overrides_applied": self.overrides_applied,
|
|
121
|
+
"run_at": self.run_at,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
class Override:
|
|
127
|
+
"""A single active session-level input override."""
|
|
128
|
+
|
|
129
|
+
source: str
|
|
130
|
+
field: str
|
|
131
|
+
value: Any
|
|
132
|
+
applied_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
|
|
133
|
+
|
|
134
|
+
def to_dict(self) -> dict:
|
|
135
|
+
"""Serialise to a JSON-compatible dict."""
|
|
136
|
+
return {
|
|
137
|
+
"source": self.source,
|
|
138
|
+
"field": self.field,
|
|
139
|
+
"value": self.value,
|
|
140
|
+
"applied_at": self.applied_at,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass
|
|
145
|
+
class ScenarioComparison:
|
|
146
|
+
"""Side-by-side comparison of two scenario results."""
|
|
147
|
+
|
|
148
|
+
scenario_a: ScenarioResult
|
|
149
|
+
scenario_b: ScenarioResult
|
|
150
|
+
metrics: list[str]
|
|
151
|
+
differences: dict[str, dict] # metric -> {a, b, delta, pct_change}
|
|
152
|
+
|
|
153
|
+
def to_dict(self) -> dict:
|
|
154
|
+
"""Serialise to a JSON-compatible dict."""
|
|
155
|
+
return {
|
|
156
|
+
"scenario_a": self.scenario_a.to_dict(),
|
|
157
|
+
"scenario_b": self.scenario_b.to_dict(),
|
|
158
|
+
"metrics": self.metrics,
|
|
159
|
+
"differences": self.differences,
|
|
160
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Abstract ports — interfaces that adapters must implement.
|
|
2
|
+
|
|
3
|
+
These define the boundary between the application layer and the
|
|
4
|
+
infrastructure/data layer. Concrete implementations live in adapters/data/.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from explicator.domain.models import (
|
|
11
|
+
InputField,
|
|
12
|
+
ModelSchema,
|
|
13
|
+
ScenarioDefinition,
|
|
14
|
+
ScenarioResult,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ScenarioRunner(ABC):
|
|
19
|
+
"""Port: executes scenarios against the underlying model."""
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def run(
|
|
23
|
+
self,
|
|
24
|
+
scenario: ScenarioDefinition,
|
|
25
|
+
extra_overrides: dict[str, Any],
|
|
26
|
+
) -> ScenarioResult:
|
|
27
|
+
"""Run a scenario with optional additional overrides beyond the definition."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ModelRepository(ABC):
|
|
31
|
+
"""Port: provides access to model configuration and metadata."""
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def get_scenarios(self) -> list[ScenarioDefinition]:
|
|
35
|
+
"""Return all available scenario definitions."""
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def get_schema(self) -> ModelSchema:
|
|
39
|
+
"""Return the full model schema."""
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def get_inputs(self) -> list[InputField]:
|
|
43
|
+
"""Return current state of all model inputs."""
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: explicator
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Provider-agnostic natural language AI interface for scenario-driven modelling
|
|
5
|
+
Author: Tim le Poidevin
|
|
6
|
+
Author-email: Tim le Poidevin <lepoidevin.tim@gmail.com>
|
|
7
|
+
Requires-Dist: mcp>=1.0.0
|
|
8
|
+
Requires-Dist: click>=8.0
|
|
9
|
+
Requires-Dist: python-dotenv>=1.0
|
|
10
|
+
Requires-Dist: openai>=1.0.0 ; extra == 'azure'
|
|
11
|
+
Requires-Dist: anthropic>=0.40.0 ; extra == 'claude'
|
|
12
|
+
Requires-Dist: pytest>=8.0 ; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest-asyncio>=0.23 ; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest-mock>=3.12 ; extra == 'dev'
|
|
15
|
+
Requires-Dist: mcp[cli] ; extra == 'dev'
|
|
16
|
+
Requires-Dist: pre-commit>=4.5.1 ; extra == 'dev'
|
|
17
|
+
Requires-Dist: ruff>=0.15.4 ; extra == 'dev'
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Provides-Extra: azure
|
|
20
|
+
Provides-Extra: claude
|
|
21
|
+
Provides-Extra: dev
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
explicator/__init__.py,sha256=2HgPB745GgqN45QWyTWXk-QZm5B-jBj9sEUNh3imp-8,5064
|
|
2
|
+
explicator/adapters/__init__.py,sha256=lDTm0Y4ieqU2KNUL1Zehmxe9Z_ICOjirFoPSA2tKSII,73
|
|
3
|
+
explicator/adapters/cli/__init__.py,sha256=XtPIF5XLQlSl6443eLb6l0qF2KLLGEm13WVVoanX4kc,34
|
|
4
|
+
explicator/adapters/cli/main.py,sha256=mHVdVs_IZVF9irONiWM1Gkk5is9qUeuNJ2gaaQkuslQ,4851
|
|
5
|
+
explicator/adapters/data/__init__.py,sha256=xzlwIEGfaZUth2AfMQpJIvdSVJK7CefvJuKWmxAUSCY,46
|
|
6
|
+
explicator/adapters/data/in_memory.py,sha256=joU9xdpK4ArxLv1-lDZrU6PO1zkmOqfUw-CJ5Wb_8I4,4113
|
|
7
|
+
explicator/adapters/mcp_server/__init__.py,sha256=RRi8FUVRjPc0HSOWXyIdfjj6TnEa8wcaRtXZ5bOiN9s,41
|
|
8
|
+
explicator/adapters/mcp_server/__main__.py,sha256=I0RU9vvxcCxWwVg-AGiRMKWRPWwE25g1l38hjVyxb0k,738
|
|
9
|
+
explicator/adapters/mcp_server/server.py,sha256=Jmm6W33mzR8QPtIZeumOGwWMAmxoVt3rgdXXs1LGjlA,10932
|
|
10
|
+
explicator/ai/__init__.py,sha256=OyfUwfz6FPqi_NpY2pjoHKHcOOpZIAkS7aEckcJ2NSQ,65
|
|
11
|
+
explicator/ai/dispatcher.py,sha256=oxXHw6poLVHV8aFHArW4e2qpGxcm1MZX_Mw6U_Jr7cg,2660
|
|
12
|
+
explicator/ai/providers/__init__.py,sha256=p_FCStIoS1gP_M1ARuP7oIXwePHm_MUEYOjKqygzsjk,60
|
|
13
|
+
explicator/ai/providers/azure_openai.py,sha256=qMlHVt05Efy8LPMWf1WQroVtAuyPW8s9qMnXuYCdWBA,3492
|
|
14
|
+
explicator/ai/providers/base.py,sha256=gDgS3380t9UI85fR5xgv7Bh8cJuTbP2UxPN-9EOKgyM,1883
|
|
15
|
+
explicator/ai/providers/claude.py,sha256=KE-N4h9Mcar23NMQYrU7Jvyubir2YuixmKzf_MZThfs,3660
|
|
16
|
+
explicator/ai/tools/__init__.py,sha256=DyXqDcgW1OMAXa3ek_aieGkHB6wpV_or_OVzcbwV2xs,70
|
|
17
|
+
explicator/ai/tools/definitions.py,sha256=nZeKhoWXgQnKHJkTT9V7o9hF6dgO1C07WJyFEZ5eijY,4599
|
|
18
|
+
explicator/application/__init__.py,sha256=djB0vJ2C-GEqC4qASBvVfoenh-muBkMksywNQLfiVJs,48
|
|
19
|
+
explicator/application/service.py,sha256=Lxcd0RXRF-hEunjjNfZxrF7xuHN4hWhBm_r8vmQ3XV0,5700
|
|
20
|
+
explicator/config.py,sha256=OoDAQI4JIMhxrfW-RUVdebWQ2_btizkJpxETJrCZvqs,2342
|
|
21
|
+
explicator/domain/__init__.py,sha256=qJJYCO6Ht2FZ6bIG-ScH0HIg-ysKbO202uhuZMeWbMM,61
|
|
22
|
+
explicator/domain/models.py,sha256=25Z9NxhLbxacGxW84wH0viAdvWiPBnBULVzf7lB_kVI,4522
|
|
23
|
+
explicator/domain/ports.py,sha256=At9_7aog8NyC6kmPZjcVgrg0yVIdVkitqWtwswGNMVQ,1183
|
|
24
|
+
explicator-0.1.0.dist-info/WHEEL,sha256=Uo4e6VmJM8J_cLBTtiWBRVQkc1yUEkw94xaL0B_lH6c,80
|
|
25
|
+
explicator-0.1.0.dist-info/entry_points.txt,sha256=RNZbSoNHKBxBRSFcKJLQJE4xnmu1ulnN1Vbk8LmWWx0,125
|
|
26
|
+
explicator-0.1.0.dist-info/METADATA,sha256=ik2OjPbnuZxoxsknBKSXJtHE0M31K7Z16tr5wsYTXac,781
|
|
27
|
+
explicator-0.1.0.dist-info/RECORD,,
|