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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.10.7
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ explicator = explicator.adapters.cli.main:cli
3
+ explicator-mcp = explicator.adapters.mcp_server.server:main
4
+