phronesisml 0.2.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.
- phronesisml/__init__.py +326 -0
- phronesisml/agents/__init__.py +0 -0
- phronesisml/agents/base.py +125 -0
- phronesisml/agents/eda/__init__.py +0 -0
- phronesisml/agents/eda/agent.py +113 -0
- phronesisml/agents/etl/__init__.py +0 -0
- phronesisml/agents/etl/agent.py +165 -0
- phronesisml/agents/evaluation/__init__.py +0 -0
- phronesisml/agents/evaluation/agent.py +169 -0
- phronesisml/agents/explainability/__init__.py +0 -0
- phronesisml/agents/explainability/agent.py +191 -0
- phronesisml/agents/feature_engineering/__init__.py +0 -0
- phronesisml/agents/feature_engineering/agent.py +148 -0
- phronesisml/agents/model_selection/__init__.py +0 -0
- phronesisml/agents/model_selection/agent.py +252 -0
- phronesisml/agents/reporting/__init__.py +0 -0
- phronesisml/agents/reporting/agent.py +90 -0
- phronesisml/agents/storage/__init__.py +0 -0
- phronesisml/agents/storage/agent.py +130 -0
- phronesisml/agents/target_detection/__init__.py +0 -0
- phronesisml/agents/target_detection/agent.py +127 -0
- phronesisml/agents/upload/__init__.py +0 -0
- phronesisml/agents/upload/agent.py +147 -0
- phronesisml/agents/validation/__init__.py +0 -0
- phronesisml/agents/validation/agent.py +111 -0
- phronesisml/configs/__init__.py +5 -0
- phronesisml/configs/settings.py +97 -0
- phronesisml/data/__init__.py +0 -0
- phronesisml/data/loaders/__init__.py +0 -0
- phronesisml/data/loaders/file_loader.py +223 -0
- phronesisml/data/profilers/__init__.py +9 -0
- phronesisml/data/profilers/stats.py +119 -0
- phronesisml/data/transformers/__init__.py +0 -0
- phronesisml/data/transformers/cleaning.py +175 -0
- phronesisml/data/validators/__init__.py +9 -0
- phronesisml/data/validators/checks.py +103 -0
- phronesisml/engines/__init__.py +0 -0
- phronesisml/engines/base_engine.py +210 -0
- phronesisml/engines/engine_selector.py +125 -0
- phronesisml/engines/pandas_engine.py +134 -0
- phronesisml/engines/polars_engine.py +139 -0
- phronesisml/engines/spark_engine.py +148 -0
- phronesisml/exceptions.py +84 -0
- phronesisml/interfaces/api/__init__.py +7 -0
- phronesisml/interfaces/api/app.py +137 -0
- phronesisml/interfaces/api/jobs.py +128 -0
- phronesisml/interfaces/api/models.py +132 -0
- phronesisml/interfaces/api/routes.py +486 -0
- phronesisml/interfaces/cli/__init__.py +0 -0
- phronesisml/interfaces/cli/app.py +114 -0
- phronesisml/ml/__init__.py +0 -0
- phronesisml/ml/automl/__init__.py +23 -0
- phronesisml/ml/automl/auto_selector.py +288 -0
- phronesisml/ml/automl/trainer.py +333 -0
- phronesisml/ml/evaluation/__init__.py +9 -0
- phronesisml/ml/evaluation/metrics.py +247 -0
- phronesisml/ml/explainability/__init__.py +16 -0
- phronesisml/ml/explainability/shap_explainer.py +214 -0
- phronesisml/ml/feature_engineering/__init__.py +9 -0
- phronesisml/ml/feature_engineering/engineer.py +358 -0
- phronesisml/ml/reports/__init__.py +9 -0
- phronesisml/ml/reports/builder.py +458 -0
- phronesisml/ml/reports/templates/full_report.md +64 -0
- phronesisml/ml/target_detection/__init__.py +12 -0
- phronesisml/ml/target_detection/detector.py +252 -0
- phronesisml/py.typed +0 -0
- phronesisml/sdk.py +786 -0
- phronesisml/simple.py +1039 -0
- phronesisml/workflow/__init__.py +0 -0
- phronesisml/workflow/graph.py +186 -0
- phronesisml/workflow/nodes.py +69 -0
- phronesisml/workflow/router.py +157 -0
- phronesisml/workflow/state.py +185 -0
- phronesisml-0.2.0.dist-info/METADATA +622 -0
- phronesisml-0.2.0.dist-info/RECORD +78 -0
- phronesisml-0.2.0.dist-info/WHEEL +4 -0
- phronesisml-0.2.0.dist-info/entry_points.txt +2 -0
- phronesisml-0.2.0.dist-info/licenses/LICENSE +21 -0
phronesisml/__init__.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""Phronesis — public SDK surface.
|
|
2
|
+
|
|
3
|
+
This is the canonical entry point for programmatic use of Phronesis.
|
|
4
|
+
All public API is exposed here; ``interfaces/cli/`` consumes this
|
|
5
|
+
surface.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
# Simple API (recommended for quickstart)
|
|
10
|
+
from phronesisml import analyze, train
|
|
11
|
+
|
|
12
|
+
profile = analyze("data.csv")
|
|
13
|
+
result = train("data.csv")
|
|
14
|
+
|
|
15
|
+
# OOP API
|
|
16
|
+
from phronesisml import Phronesis
|
|
17
|
+
|
|
18
|
+
ml = Phronesis("data.csv")
|
|
19
|
+
ml.run()
|
|
20
|
+
print(ml.report())
|
|
21
|
+
|
|
22
|
+
# Advanced API (full control)
|
|
23
|
+
import phronesisml
|
|
24
|
+
|
|
25
|
+
result = await phronesisml.run_pipeline(data_path="data.csv")
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import logging
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
from phronesisml.configs.settings import PhronesisConfig
|
|
34
|
+
from phronesisml.exceptions import ConfigurationError, PhronesisError, WorkflowError
|
|
35
|
+
from phronesisml.sdk import (
|
|
36
|
+
DatasetSummary,
|
|
37
|
+
EDAReport,
|
|
38
|
+
EvaluationMetrics,
|
|
39
|
+
ExplanationReport,
|
|
40
|
+
FeatureReport,
|
|
41
|
+
ModelInfo,
|
|
42
|
+
Phronesis,
|
|
43
|
+
TargetInfo,
|
|
44
|
+
ValidationReport,
|
|
45
|
+
)
|
|
46
|
+
from phronesisml.simple import (
|
|
47
|
+
CleanResult,
|
|
48
|
+
DatasetProfile,
|
|
49
|
+
ExplainResult,
|
|
50
|
+
FeatureResult,
|
|
51
|
+
ModelResult,
|
|
52
|
+
TargetResult,
|
|
53
|
+
TrainResult,
|
|
54
|
+
ValidationResult,
|
|
55
|
+
analyze,
|
|
56
|
+
analyze_async,
|
|
57
|
+
clean,
|
|
58
|
+
clean_async,
|
|
59
|
+
detect_target,
|
|
60
|
+
detect_target_async,
|
|
61
|
+
engineer,
|
|
62
|
+
engineer_async,
|
|
63
|
+
explain,
|
|
64
|
+
explain_async,
|
|
65
|
+
report,
|
|
66
|
+
report_async,
|
|
67
|
+
select_model,
|
|
68
|
+
select_model_async,
|
|
69
|
+
train,
|
|
70
|
+
train_async,
|
|
71
|
+
validate,
|
|
72
|
+
validate_async,
|
|
73
|
+
)
|
|
74
|
+
from phronesisml.workflow.state import WorkflowState
|
|
75
|
+
|
|
76
|
+
logger = logging.getLogger(__name__)
|
|
77
|
+
|
|
78
|
+
__version__ = "0.2.0"
|
|
79
|
+
|
|
80
|
+
__all__ = [
|
|
81
|
+
# ── Simple API ──────────────────────────────────────
|
|
82
|
+
"analyze",
|
|
83
|
+
"analyze_async",
|
|
84
|
+
"clean",
|
|
85
|
+
"clean_async",
|
|
86
|
+
"validate",
|
|
87
|
+
"validate_async",
|
|
88
|
+
"detect_target",
|
|
89
|
+
"detect_target_async",
|
|
90
|
+
"engineer",
|
|
91
|
+
"engineer_async",
|
|
92
|
+
"select_model",
|
|
93
|
+
"select_model_async",
|
|
94
|
+
"explain",
|
|
95
|
+
"explain_async",
|
|
96
|
+
"report",
|
|
97
|
+
"report_async",
|
|
98
|
+
"train",
|
|
99
|
+
"train_async",
|
|
100
|
+
"CleanResult",
|
|
101
|
+
"DatasetProfile",
|
|
102
|
+
"ExplainResult",
|
|
103
|
+
"FeatureResult",
|
|
104
|
+
"ModelResult",
|
|
105
|
+
"TargetResult",
|
|
106
|
+
"TrainResult",
|
|
107
|
+
"ValidationResult",
|
|
108
|
+
# ── OOP API ─────────────────────────────────────────
|
|
109
|
+
"Phronesis",
|
|
110
|
+
"DatasetSummary",
|
|
111
|
+
"EDAReport",
|
|
112
|
+
"EvaluationMetrics",
|
|
113
|
+
"ExplanationReport",
|
|
114
|
+
"FeatureReport",
|
|
115
|
+
"ModelInfo",
|
|
116
|
+
"TargetInfo",
|
|
117
|
+
"ValidationReport",
|
|
118
|
+
# ── Advanced API ────────────────────────────────────
|
|
119
|
+
"PhronesisConfig",
|
|
120
|
+
"PhronesisError",
|
|
121
|
+
"ConfigurationError",
|
|
122
|
+
"WorkflowError",
|
|
123
|
+
"WorkflowState",
|
|
124
|
+
"run_pipeline",
|
|
125
|
+
"__version__",
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _compose_agents(
|
|
130
|
+
config: PhronesisConfig,
|
|
131
|
+
data_path: str,
|
|
132
|
+
) -> dict[str, Any]:
|
|
133
|
+
"""Manually compose all agents via constructor injection.
|
|
134
|
+
|
|
135
|
+
This is the composition root — the only place where concrete agent
|
|
136
|
+
and engine classes are instantiated. The rest of the SDK depends
|
|
137
|
+
on abstractions (``BaseAgent``, ``BaseEngine``).
|
|
138
|
+
|
|
139
|
+
All agent imports are deferred to this function so that
|
|
140
|
+
``import phronesisml`` does not pull in every dependency eagerly.
|
|
141
|
+
"""
|
|
142
|
+
from phronesisml.agents.eda.agent import EDAAgent
|
|
143
|
+
from phronesisml.agents.etl.agent import ETLAgent, ETLConfig
|
|
144
|
+
from phronesisml.agents.evaluation.agent import EvaluationAgent
|
|
145
|
+
from phronesisml.agents.explainability.agent import ExplainabilityAgent
|
|
146
|
+
from phronesisml.agents.feature_engineering.agent import FeatureEngineeringAgent
|
|
147
|
+
from phronesisml.agents.model_selection.agent import ModelSelectionAgent
|
|
148
|
+
from phronesisml.agents.reporting.agent import ReportingAgent
|
|
149
|
+
from phronesisml.agents.storage.agent import StorageAgent
|
|
150
|
+
from phronesisml.agents.target_detection.agent import TargetDetectionAgent
|
|
151
|
+
from phronesisml.agents.upload.agent import UploadAgent
|
|
152
|
+
from phronesisml.agents.validation.agent import ValidationAgent
|
|
153
|
+
from phronesisml.engines.engine_selector import select_engine
|
|
154
|
+
|
|
155
|
+
# Select the computation engine
|
|
156
|
+
engine = select_engine(config=config, data_path=data_path)
|
|
157
|
+
|
|
158
|
+
# Instantiate agents with their dependencies
|
|
159
|
+
agents: dict[str, Any] = {
|
|
160
|
+
"upload": UploadAgent(engine=engine),
|
|
161
|
+
"validation": ValidationAgent(engine=engine),
|
|
162
|
+
"etl": ETLAgent(config=ETLConfig(null_strategy="drop")),
|
|
163
|
+
"eda": EDAAgent(engine=engine),
|
|
164
|
+
"target_detection": TargetDetectionAgent(engine=engine),
|
|
165
|
+
"feature_engineering": FeatureEngineeringAgent(
|
|
166
|
+
engine=engine,
|
|
167
|
+
feature_selection_config=config.feature_selection,
|
|
168
|
+
),
|
|
169
|
+
"model_selection": ModelSelectionAgent(engine=engine),
|
|
170
|
+
"evaluation": EvaluationAgent(engine=engine),
|
|
171
|
+
"explainability": ExplainabilityAgent(engine=engine),
|
|
172
|
+
"reporting": ReportingAgent(),
|
|
173
|
+
"storage": StorageAgent(),
|
|
174
|
+
}
|
|
175
|
+
return agents
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
_FULL_PIPELINE_STAGES: list[str] = [
|
|
179
|
+
"upload",
|
|
180
|
+
"etl",
|
|
181
|
+
"validation",
|
|
182
|
+
"eda",
|
|
183
|
+
"target_detection",
|
|
184
|
+
"feature_engineering",
|
|
185
|
+
"model_selection",
|
|
186
|
+
"evaluation",
|
|
187
|
+
"explainability",
|
|
188
|
+
"reporting",
|
|
189
|
+
"storage",
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
async def run_pipeline(
|
|
194
|
+
data_path: str,
|
|
195
|
+
engine_preference: str | None = None,
|
|
196
|
+
null_strategy: str = "drop",
|
|
197
|
+
stages: list[str] | None = None,
|
|
198
|
+
config: PhronesisConfig | None = None,
|
|
199
|
+
) -> dict[str, Any]:
|
|
200
|
+
"""Run the Phronesis pipeline on a dataset.
|
|
201
|
+
|
|
202
|
+
This is the primary public API. It:
|
|
203
|
+
1. Builds configuration (from *config* or defaults).
|
|
204
|
+
2. Composes agents via manual DI.
|
|
205
|
+
3. Constructs the LangGraph workflow with the requested stages.
|
|
206
|
+
4. Executes the graph with the initial state.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
data_path: Path to the input dataset.
|
|
210
|
+
engine_preference: Force a specific engine (``"pandas"``, ``"polars"``,
|
|
211
|
+
``"spark"``). ``None`` for auto-selection.
|
|
212
|
+
null_strategy: Null handling strategy (``"drop"``, ``"fill"``, ``"flag"``).
|
|
213
|
+
stages: Ordered list of pipeline stages to execute. If ``None``,
|
|
214
|
+
runs the full pipeline (all 11 stages).
|
|
215
|
+
config: Optional pre-built configuration. If ``None``, a config is
|
|
216
|
+
constructed from the other arguments.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
A dict summarising the pipeline results.
|
|
220
|
+
|
|
221
|
+
Raises:
|
|
222
|
+
WorkflowError: If the workflow graph execution fails.
|
|
223
|
+
|
|
224
|
+
"""
|
|
225
|
+
if config is None:
|
|
226
|
+
config = PhronesisConfig()
|
|
227
|
+
if engine_preference is not None:
|
|
228
|
+
config.engine.preferred = engine_preference
|
|
229
|
+
if stages is None:
|
|
230
|
+
stages = list(_FULL_PIPELINE_STAGES)
|
|
231
|
+
|
|
232
|
+
logger.info("Phronesis pipeline starting — data_path=%s", data_path)
|
|
233
|
+
|
|
234
|
+
# 1–3. Compose agents, build graph, build initial state
|
|
235
|
+
try:
|
|
236
|
+
agents = _compose_agents(config, data_path)
|
|
237
|
+
from phronesisml.workflow.graph import build_graph
|
|
238
|
+
|
|
239
|
+
graph = build_graph(agents, stages=stages)
|
|
240
|
+
initial_state = WorkflowState(data_path=data_path)
|
|
241
|
+
except WorkflowError:
|
|
242
|
+
raise
|
|
243
|
+
except ConfigurationError as exc:
|
|
244
|
+
msg = f"Pipeline configuration failed: {exc}"
|
|
245
|
+
logger.exception(msg)
|
|
246
|
+
raise WorkflowError(msg) from exc
|
|
247
|
+
except Exception as exc:
|
|
248
|
+
msg = f"Pipeline setup failed: {exc}"
|
|
249
|
+
logger.exception(msg)
|
|
250
|
+
raise WorkflowError(msg) from exc
|
|
251
|
+
|
|
252
|
+
# 4. Execute
|
|
253
|
+
try:
|
|
254
|
+
final_state = await graph.ainvoke(initial_state)
|
|
255
|
+
except WorkflowError:
|
|
256
|
+
raise
|
|
257
|
+
except Exception as exc:
|
|
258
|
+
msg = f"Workflow execution failed: {exc}"
|
|
259
|
+
logger.exception(msg)
|
|
260
|
+
raise WorkflowError(msg) from exc
|
|
261
|
+
|
|
262
|
+
logger.info("Phronesis pipeline complete.")
|
|
263
|
+
|
|
264
|
+
# Extract summary from the final state
|
|
265
|
+
if isinstance(final_state, dict):
|
|
266
|
+
return _extract_summary(final_state)
|
|
267
|
+
return _extract_summary(final_state.model_dump())
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _extract_summary(state: dict[str, Any]) -> dict[str, Any]:
|
|
271
|
+
"""Build a summary dict from the final workflow state."""
|
|
272
|
+
processed = state.get("processed_data")
|
|
273
|
+
validated = state.get("validated_data")
|
|
274
|
+
profile = state.get("data_profile")
|
|
275
|
+
best_pipeline = state.get("best_pipeline")
|
|
276
|
+
evaluation_report = state.get("evaluation_report")
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
"row_count": state.get("row_count"),
|
|
280
|
+
"column_count": (
|
|
281
|
+
len(processed.columns)
|
|
282
|
+
if processed is not None
|
|
283
|
+
else (len(validated.columns) if validated is not None else None)
|
|
284
|
+
),
|
|
285
|
+
"transformations": (
|
|
286
|
+
len(state["transform_log"]) if state.get("transform_log") is not None else 0
|
|
287
|
+
),
|
|
288
|
+
"validation_passed": (
|
|
289
|
+
state["validation_report"].get("passed")
|
|
290
|
+
if state.get("validation_report") is not None
|
|
291
|
+
else None
|
|
292
|
+
),
|
|
293
|
+
"numeric_columns": (profile.get("numeric_columns") if profile is not None else None),
|
|
294
|
+
"categorical_columns": (
|
|
295
|
+
profile.get("categorical_columns") if profile is not None else None
|
|
296
|
+
),
|
|
297
|
+
"target_column": state.get("target_column"),
|
|
298
|
+
"task_type": state.get("task_type"),
|
|
299
|
+
"target_detection_confidence": state.get("target_detection_confidence"),
|
|
300
|
+
"ambiguity_reason": state.get("ambiguity_reason"),
|
|
301
|
+
"n_features": (
|
|
302
|
+
len(state["feature_names"]) if state.get("feature_names") is not None else None
|
|
303
|
+
),
|
|
304
|
+
"best_model_type": (best_pipeline.get("model_type") if best_pipeline is not None else None),
|
|
305
|
+
"best_model_score": (best_pipeline.get("score") if best_pipeline is not None else None),
|
|
306
|
+
"hpo_truncated": (best_pipeline.get("truncated") if best_pipeline is not None else None),
|
|
307
|
+
"evaluation_metrics": (
|
|
308
|
+
evaluation_report.get("metrics") if evaluation_report is not None else None
|
|
309
|
+
),
|
|
310
|
+
"evaluation_ambiguity_caveat": (
|
|
311
|
+
evaluation_report.get("ambiguity_caveat") if evaluation_report is not None else None
|
|
312
|
+
),
|
|
313
|
+
"explanation_sampled": (
|
|
314
|
+
state["explanation_report"].get("sampled")
|
|
315
|
+
if state.get("explanation_report") is not None
|
|
316
|
+
else None
|
|
317
|
+
),
|
|
318
|
+
"explanation_explainer_type": (
|
|
319
|
+
state["explanation_report"].get("explainer_type")
|
|
320
|
+
if state.get("explanation_report") is not None
|
|
321
|
+
else None
|
|
322
|
+
),
|
|
323
|
+
"final_report_length": (
|
|
324
|
+
len(state["final_report"]) if state.get("final_report") is not None else None
|
|
325
|
+
),
|
|
326
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Base contracts for the Phronesis agent system.
|
|
2
|
+
|
|
3
|
+
Every agent in the framework must satisfy the ``BaseAgent`` protocol.
|
|
4
|
+
The protocol uses structural subtyping (``Protocol``) so agents do not
|
|
5
|
+
need to inherit from a base class — they only need to implement the
|
|
6
|
+
required attributes and methods with the correct signatures.
|
|
7
|
+
|
|
8
|
+
Design rationale:
|
|
9
|
+
- ``Protocol`` over ABC: avoids diamond-inheritance issues when agents
|
|
10
|
+
also need domain-specific base classes (e.g. domain-specific agents).
|
|
11
|
+
- ``async run``: agents may perform I/O (file reads, DB
|
|
12
|
+
queries) and async keeps the workflow non-blocking.
|
|
13
|
+
- ``AgentResult``: a lightweight return type that carries the agent's
|
|
14
|
+
output payload and optional metadata, keeping the contract uniform
|
|
15
|
+
across all agents regardless of what they produce.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Any, Protocol, runtime_checkable
|
|
21
|
+
|
|
22
|
+
__all__ = ["AgentResult", "BaseAgent", "Tool"]
|
|
23
|
+
|
|
24
|
+
from pydantic import BaseModel, Field
|
|
25
|
+
|
|
26
|
+
from phronesisml.exceptions import AgentNotImplementedError
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AgentResult(BaseModel):
|
|
30
|
+
"""Standardised return value from every agent ``run()`` call.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
success: Whether the agent completed without error.
|
|
34
|
+
data: Agent-specific output payload. The shape varies per agent
|
|
35
|
+
and is documented in each agent's own module docstring.
|
|
36
|
+
error: Human-readable error message when ``success`` is False.
|
|
37
|
+
error_type: Exception class name when ``success`` is False.
|
|
38
|
+
E.g. ``"DataLoadError"``, ``"ValueError"``. Survives JSON
|
|
39
|
+
serialization for non-Python consumers (FastAPI boundary).
|
|
40
|
+
error_message: Original exception message when ``success`` is False.
|
|
41
|
+
May differ from ``error`` (which is the agent-wrapped message).
|
|
42
|
+
error_context: Optional structured context dict for debugging.
|
|
43
|
+
Keys and values must be strings to survive serialization.
|
|
44
|
+
metadata: Arbitrary key-value metadata (e.g. row counts, timings).
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
success: bool = True
|
|
49
|
+
data: dict[str, Any] = Field(default_factory=dict)
|
|
50
|
+
error: str | None = None
|
|
51
|
+
error_type: str | None = None
|
|
52
|
+
error_message: str | None = None
|
|
53
|
+
error_context: dict[str, str] | None = None
|
|
54
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Tool(BaseModel):
|
|
58
|
+
"""Descriptor for a tool an agent exposes to the workflow.
|
|
59
|
+
|
|
60
|
+
This is a data descriptor, not a callable — the actual tool
|
|
61
|
+
implementation lives in the agent; the ``Tool`` object is a
|
|
62
|
+
serialisable declaration used for discovery and documentation.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
name: str
|
|
66
|
+
description: str
|
|
67
|
+
parameters: dict[str, Any] = Field(default_factory=dict)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@runtime_checkable
|
|
71
|
+
class BaseAgent(Protocol):
|
|
72
|
+
"""Protocol that every Phronesis agent must satisfy.
|
|
73
|
+
|
|
74
|
+
Agents are stateless with respect to the workflow — all mutable
|
|
75
|
+
state lives in ``WorkflowState`` and is passed in on every ``run()``
|
|
76
|
+
call. This makes agents independently testable and composable.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
name: str
|
|
80
|
+
description: str
|
|
81
|
+
|
|
82
|
+
async def run(self, state: Any) -> AgentResult:
|
|
83
|
+
"""Execute the agent's core logic against the current workflow state.
|
|
84
|
+
|
|
85
|
+
Agents MUST return an ``AgentResult`` — they MUST NOT raise
|
|
86
|
+
exceptions for expected/transient failures (bad data, missing
|
|
87
|
+
columns, model failures). Instead, return
|
|
88
|
+
``AgentResult(success=False, error=..., error_type=..., error_message=...)``
|
|
89
|
+
so the workflow can decide whether to continue or abort.
|
|
90
|
+
|
|
91
|
+
Agents SHOULD raise only for programming errors (bugs, missing
|
|
92
|
+
dependencies) or truly unrecoverable failures (out of memory).
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
state: The shared ``WorkflowState`` instance.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
An ``AgentResult`` carrying the agent's output.
|
|
99
|
+
|
|
100
|
+
"""
|
|
101
|
+
...
|
|
102
|
+
|
|
103
|
+
def get_tools(self) -> list[Tool]:
|
|
104
|
+
"""Return the list of tools this agent exposes."""
|
|
105
|
+
...
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class _StubAgent:
|
|
109
|
+
"""Minimal stub that satisfies ``BaseAgent`` for agents not yet implemented.
|
|
110
|
+
|
|
111
|
+
This is used only during the initial skeleton pass to validate that
|
|
112
|
+
the protocol works structurally across all 15 agent directories.
|
|
113
|
+
It will be replaced by real implementations in subsequent passes.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(self, name: str, description: str) -> None:
|
|
117
|
+
self.name = name
|
|
118
|
+
self.description = description
|
|
119
|
+
|
|
120
|
+
async def run(self, state: Any) -> AgentResult:
|
|
121
|
+
msg = f"Agent '{self.name}' has not been implemented yet."
|
|
122
|
+
raise AgentNotImplementedError(msg)
|
|
123
|
+
|
|
124
|
+
def get_tools(self) -> list[Tool]:
|
|
125
|
+
return []
|
|
File without changes
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""EDA agent — exploratory data analysis and statistical profiling.
|
|
2
|
+
|
|
3
|
+
This agent receives the validated DataFrame from the workflow state and
|
|
4
|
+
produces descriptive statistics and distribution analysis using the
|
|
5
|
+
active computation engine:
|
|
6
|
+
|
|
7
|
+
1. Descriptive statistics per column (min/max/mean/std for numeric;
|
|
8
|
+
cardinality/top values for categorical).
|
|
9
|
+
2. Dataset-level summary (shape, dtypes, memory usage).
|
|
10
|
+
|
|
11
|
+
Design:
|
|
12
|
+
- Stateless: all inputs come from ``WorkflowState``.
|
|
13
|
+
- Engine-mediated: all data operations go through ``BaseEngine`` —
|
|
14
|
+
no direct pandas/polars imports.
|
|
15
|
+
- Thin orchestrator: delegates to ``data.profilers.stats`` for real
|
|
16
|
+
computation logic.
|
|
17
|
+
- Returns: dict with ``data_profile`` and ``eda_report``
|
|
18
|
+
that LangGraph merges into the workflow state.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from phronesisml.agents.base import AgentResult, Tool
|
|
27
|
+
from phronesisml.data.profilers.stats import profile_dataset
|
|
28
|
+
from phronesisml.engines.base_engine import BaseEngine
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class EDAAgent:
|
|
34
|
+
"""Agent responsible for exploratory data analysis and profiling.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
engine: The active computation engine used for data operations.
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
name = "eda"
|
|
42
|
+
description = "Perform exploratory data analysis and statistical profiling."
|
|
43
|
+
|
|
44
|
+
def __init__(self, engine: BaseEngine) -> None:
|
|
45
|
+
self._engine = engine
|
|
46
|
+
|
|
47
|
+
async def run(self, state: Any) -> AgentResult:
|
|
48
|
+
"""Profile ``state.validated_data`` (or ``state.processed_data``).
|
|
49
|
+
|
|
50
|
+
Reads from: ``state.validated_data`` (preferred) or
|
|
51
|
+
``state.processed_data`` (fallback).
|
|
52
|
+
Returns: dict with ``data_profile`` and ``eda_report``
|
|
53
|
+
"""
|
|
54
|
+
data = state.validated_data if state.validated_data is not None else state.processed_data
|
|
55
|
+
if data is None:
|
|
56
|
+
return AgentResult(
|
|
57
|
+
success=False,
|
|
58
|
+
error="No validated_data or processed_data in workflow state.",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
profile = profile_dataset(data, self._engine)
|
|
63
|
+
|
|
64
|
+
eda_report = {
|
|
65
|
+
"summary": f"Dataset has {profile['shape']['rows']} rows and "
|
|
66
|
+
f"{profile['shape']['columns']} columns.",
|
|
67
|
+
"numeric_columns": profile["numeric_columns"],
|
|
68
|
+
"categorical_columns": profile["categorical_columns"],
|
|
69
|
+
"memory_bytes": profile["memory_bytes"],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
logger.info(
|
|
73
|
+
"EDA complete: %d rows, %d columns, %d numeric, %d categorical.",
|
|
74
|
+
profile["shape"]["rows"],
|
|
75
|
+
profile["shape"]["columns"],
|
|
76
|
+
len(profile["numeric_columns"]),
|
|
77
|
+
len(profile["categorical_columns"]),
|
|
78
|
+
)
|
|
79
|
+
return AgentResult(
|
|
80
|
+
success=True,
|
|
81
|
+
data={
|
|
82
|
+
"data_profile": profile,
|
|
83
|
+
"eda_report": eda_report,
|
|
84
|
+
},
|
|
85
|
+
metadata={
|
|
86
|
+
"rows": profile["shape"]["rows"],
|
|
87
|
+
"columns": profile["shape"]["columns"],
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
except Exception as exc:
|
|
91
|
+
msg = f"Unexpected error during EDA: {exc}"
|
|
92
|
+
logger.exception(msg)
|
|
93
|
+
return AgentResult(
|
|
94
|
+
success=False,
|
|
95
|
+
error=msg,
|
|
96
|
+
error_type=type(exc).__name__,
|
|
97
|
+
error_message=str(exc),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def get_tools(self) -> list[Tool]:
|
|
101
|
+
return [
|
|
102
|
+
Tool(
|
|
103
|
+
name="profile_data",
|
|
104
|
+
description="Compute descriptive statistics and distribution info for a DataFrame.",
|
|
105
|
+
parameters={
|
|
106
|
+
"columns": {
|
|
107
|
+
"type": "array",
|
|
108
|
+
"items": {"type": "string"},
|
|
109
|
+
"description": "Specific columns to profile (default: all).",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
),
|
|
113
|
+
]
|
|
File without changes
|