open-data-sci 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.
- open_data_sci-0.1.0.dist-info/METADATA +629 -0
- open_data_sci-0.1.0.dist-info/RECORD +85 -0
- open_data_sci-0.1.0.dist-info/WHEEL +4 -0
- open_data_sci-0.1.0.dist-info/entry_points.txt +2 -0
- open_data_sci-0.1.0.dist-info/licenses/LICENSE +201 -0
- opendatasci/__init__.py +47 -0
- opendatasci/_tui/__init__.py +1 -0
- opendatasci/_tui/adapter.py +102 -0
- opendatasci/_tui/app.py +429 -0
- opendatasci/_tui/commands.py +95 -0
- opendatasci/_tui/completion.py +139 -0
- opendatasci/_tui/controller.py +644 -0
- opendatasci/_tui/file_refs.py +153 -0
- opendatasci/_tui/models.py +4 -0
- opendatasci/_tui/presenter.py +259 -0
- opendatasci/_tui/service.py +78 -0
- opendatasci/_tui/session.py +53 -0
- opendatasci/_tui/styles.tcss +248 -0
- opendatasci/_tui/styles_visible.tcss +245 -0
- opendatasci/_tui/theme.py +113 -0
- opendatasci/_tui/tools_display.py +86 -0
- opendatasci/_tui/widgets.py +1001 -0
- opendatasci/_utils/__init__.py +0 -0
- opendatasci/_utils/async_utils.py +11 -0
- opendatasci/_utils/data_formats.py +135 -0
- opendatasci/_utils/hash_utils.py +52 -0
- opendatasci/_utils/langchain_utils.py +155 -0
- opendatasci/_utils/streaming_utils.py +23 -0
- opendatasci/agents/__init__.py +12 -0
- opendatasci/agents/agents.py +515 -0
- opendatasci/agents/agents_factory.py +71 -0
- opendatasci/agents/chat_memory.py +397 -0
- opendatasci/agents/graphs.py +84 -0
- opendatasci/agents/nodes.py +74 -0
- opendatasci/agents/states.py +36 -0
- opendatasci/agents/turn_memory.py +124 -0
- opendatasci/configs.py +275 -0
- opendatasci/context/__init__.py +7 -0
- opendatasci/context/base.py +56 -0
- opendatasci/context/local.py +236 -0
- opendatasci/models/__init__.py +7 -0
- opendatasci/models/anthropic.py +40 -0
- opendatasci/models/aws.py +86 -0
- opendatasci/models/factory.py +179 -0
- opendatasci/models/google.py +79 -0
- opendatasci/models/local.py +79 -0
- opendatasci/models/microsoft.py +62 -0
- opendatasci/models/openai.py +49 -0
- opendatasci/models/providers.py +12 -0
- opendatasci/prompts/__init__.py +5 -0
- opendatasci/prompts/builders.py +85 -0
- opendatasci/prompts/caching.py +42 -0
- opendatasci/prompts/message_templates.py +7 -0
- opendatasci/prompts/prompt_templates.py +227 -0
- opendatasci/resources/skills/competitive_data_science.md +241 -0
- opendatasci/resources/skills/data_science.md +55 -0
- opendatasci/resources/skills/data_science_education.md +42 -0
- opendatasci/resources/skills/deep_learning.md +205 -0
- opendatasci/resources/skills/machine_learning.md +68 -0
- opendatasci/resources/skills/quantitative_analysis.md +45 -0
- opendatasci/sandbox/__init__.py +14 -0
- opendatasci/sandbox/_runner.py +114 -0
- opendatasci/sandbox/base.py +170 -0
- opendatasci/sandbox/srt.py +490 -0
- opendatasci/skills/__init__.py +9 -0
- opendatasci/skills/base.py +28 -0
- opendatasci/skills/local.py +131 -0
- opendatasci/streaming/__init__.py +37 -0
- opendatasci/streaming/events.py +159 -0
- opendatasci/streaming/processors.py +387 -0
- opendatasci/tools/__init__.py +58 -0
- opendatasci/tools/coding.py +261 -0
- opendatasci/tools/critic.py +136 -0
- opendatasci/tools/dataset_info.py +391 -0
- opendatasci/tools/factory.py +172 -0
- opendatasci/tools/mcp.py +179 -0
- opendatasci/tools/planning.py +88 -0
- opendatasci/tools/skills.py +90 -0
- opendatasci/tools/user_interaction.py +54 -0
- opendatasci/tools/web.py +236 -0
- opendatasci/tools/workers.py +237 -0
- opendatasci/tools/workspace.py +55 -0
- opendatasci/workspace/__init__.py +9 -0
- opendatasci/workspace/base.py +20 -0
- opendatasci/workspace/local.py +25 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"""Dataset context tools and profiling code generation.
|
|
2
|
+
|
|
3
|
+
Exposes:
|
|
4
|
+
- ``build_profile_code(path)`` – sandbox-ready profiling snippet generator.
|
|
5
|
+
- ``get_load_dataset_info_tool(session)`` – load persistent dataset info.
|
|
6
|
+
- ``get_profile_dataset_tool(session)`` – profile a dataset and cache the card.
|
|
7
|
+
- ``get_data_context_tools(session)`` – all three tools as a list.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from langchain_core.tools import BaseTool, tool
|
|
11
|
+
|
|
12
|
+
from opendatasci.context.base import BaseContextStore
|
|
13
|
+
from opendatasci.sandbox.base import BaseSandbox
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Profiling code generation
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# All curly-braces that belong to *Python* code (f-strings, dict literals,
|
|
19
|
+
# format specs) are doubled so they survive the .format(path_repr=...) call.
|
|
20
|
+
# Only ``{path_repr}`` is a real format placeholder.
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
_PROFILE_CODE_TEMPLATE = """\
|
|
24
|
+
import datetime
|
|
25
|
+
import pathlib
|
|
26
|
+
|
|
27
|
+
import pandas as pd
|
|
28
|
+
|
|
29
|
+
_path = pathlib.Path({path_repr})
|
|
30
|
+
_ext = _path.suffix.lower()
|
|
31
|
+
|
|
32
|
+
_LOADERS = {{
|
|
33
|
+
".csv": lambda p: pd.read_csv(p, low_memory=False),
|
|
34
|
+
".tsv": lambda p: pd.read_csv(p, sep="\\t", low_memory=False),
|
|
35
|
+
".txt": lambda p: pd.read_csv(p, low_memory=False),
|
|
36
|
+
".parquet": lambda p: pd.read_parquet(p),
|
|
37
|
+
".pq": lambda p: pd.read_parquet(p),
|
|
38
|
+
".json": lambda p: pd.read_json(p),
|
|
39
|
+
".jsonl": lambda p: pd.read_json(p, lines=True),
|
|
40
|
+
".ndjson": lambda p: pd.read_json(p, lines=True),
|
|
41
|
+
".xlsx": lambda p: pd.read_excel(p),
|
|
42
|
+
".xls": lambda p: pd.read_excel(p),
|
|
43
|
+
".xlsm": lambda p: pd.read_excel(p),
|
|
44
|
+
".feather": lambda p: pd.read_feather(p),
|
|
45
|
+
".ftr": lambda p: pd.read_feather(p),
|
|
46
|
+
".pkl": lambda p: pd.read_pickle(p),
|
|
47
|
+
}}
|
|
48
|
+
|
|
49
|
+
_loader = _LOADERS.get(_ext)
|
|
50
|
+
try:
|
|
51
|
+
if _loader is not None:
|
|
52
|
+
_df = _loader(_path)
|
|
53
|
+
else:
|
|
54
|
+
_df = pd.read_csv(_path, low_memory=False)
|
|
55
|
+
except Exception as _load_err:
|
|
56
|
+
result = f"__PROFILE_SKIP__Cannot load {{_path.name}} ({{type(_load_err).__name__}}: {{_load_err}})"
|
|
57
|
+
else:
|
|
58
|
+
_rows, _cols = _df.shape
|
|
59
|
+
_mem_mb = _df.memory_usage(deep=True).sum() / 1_048_576
|
|
60
|
+
_dupes = int(_df.duplicated().sum())
|
|
61
|
+
_ts = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
|
62
|
+
|
|
63
|
+
# --- Per-column unique counts (computed once, reused below) ---
|
|
64
|
+
_unique_counts = {{col: int(_df[col].nunique(dropna=True)) for col in _df.columns}}
|
|
65
|
+
|
|
66
|
+
# --- Pandera schema inference ---
|
|
67
|
+
# Used for (a) nullable-integer detection and (b) the Inferred Schema section.
|
|
68
|
+
_pa_schema = None
|
|
69
|
+
_pa_nullable = {{}} # col -> bool
|
|
70
|
+
try:
|
|
71
|
+
import pandera as pa
|
|
72
|
+
_pa_schema = pa.infer_schema(_df)
|
|
73
|
+
_pa_nullable = {{n: c.nullable for n, c in _pa_schema.columns.items()}}
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
# --- Data quality flags ---
|
|
78
|
+
_dq_flags = []
|
|
79
|
+
for _col in _df.columns:
|
|
80
|
+
_nuniq = _unique_counts[_col]
|
|
81
|
+
_nonnull = int(_df[_col].notna().sum())
|
|
82
|
+
_null_pct = ((_rows - _nonnull) / _rows * 100) if _rows > 0 else 0.0
|
|
83
|
+
_dtype_s = str(_df[_col].dtype)
|
|
84
|
+
|
|
85
|
+
if _nuniq == 1:
|
|
86
|
+
_dq_flags.append((_col, "Constant", "Only 1 unique value across all rows"))
|
|
87
|
+
if _rows > 0 and _nuniq == _rows and _null_pct == 0:
|
|
88
|
+
_dq_flags.append((_col, "Likely ID", "100% unique, no nulls"))
|
|
89
|
+
elif _rows > 0 and _nuniq / _rows > 0.95 and _null_pct < 1 and _dtype_s == "object":
|
|
90
|
+
_dq_flags.append((_col, "High cardinality", f"{{_nuniq / _rows * 100:.1f}}% unique values"))
|
|
91
|
+
if _null_pct > 50:
|
|
92
|
+
_dq_flags.append((_col, "High nulls", f"{{_null_pct:.1f}}% missing"))
|
|
93
|
+
# Pandera marks these nullable=True; cross-check that all non-null values are
|
|
94
|
+
# whole numbers to confirm the column is really an integer column with nulls.
|
|
95
|
+
if _dtype_s.startswith("float") and _pa_nullable.get(_col, False):
|
|
96
|
+
_sample = _df[_col].dropna()
|
|
97
|
+
if len(_sample) > 0 and (_sample % 1 == 0).all():
|
|
98
|
+
_dq_flags.append((
|
|
99
|
+
_col, "Nullable int",
|
|
100
|
+
f"Stored as {{_dtype_s}}; all non-null values are integers — "
|
|
101
|
+
"consider casting to a nullable integer dtype",
|
|
102
|
+
))
|
|
103
|
+
|
|
104
|
+
_lines = [
|
|
105
|
+
f"# Dataset Profile: {{_path.name}}",
|
|
106
|
+
"",
|
|
107
|
+
f"**Profiled:** {{_ts}} ",
|
|
108
|
+
f"**Path:** `{{_path}}`",
|
|
109
|
+
"",
|
|
110
|
+
"## Overview",
|
|
111
|
+
"",
|
|
112
|
+
"| Metric | Value |",
|
|
113
|
+
"|--------|-------|",
|
|
114
|
+
f"| Rows | {{_rows:,}} |",
|
|
115
|
+
f"| Columns | {{_cols:,}} |",
|
|
116
|
+
f"| Memory | {{_mem_mb:.2f}} MB |",
|
|
117
|
+
f"| Duplicate rows | {{_dupes:,}} |",
|
|
118
|
+
"",
|
|
119
|
+
"## Columns",
|
|
120
|
+
"",
|
|
121
|
+
"| Column | Type | Non-null | Null % | Unique | Unique % |",
|
|
122
|
+
"|--------|------|----------|--------|--------|----------|",
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
for _col in _df.columns:
|
|
126
|
+
_dtype = str(_df[_col].dtype)
|
|
127
|
+
_nonnull = int(_df[_col].notna().sum())
|
|
128
|
+
_null_pct = ((_rows - _nonnull) / _rows * 100) if _rows > 0 else 0.0
|
|
129
|
+
_nuniq = _unique_counts[_col]
|
|
130
|
+
_uniq_pct = (_nuniq / _rows * 100) if _rows > 0 else 0.0
|
|
131
|
+
_lines.append(
|
|
132
|
+
f"| `{{_col}}` | {{_dtype}} | {{_nonnull:,}} | {{_null_pct:.1f}}% "
|
|
133
|
+
f"| {{_nuniq:,}} | {{_uniq_pct:.1f}}% |"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# --- Data quality flags ---
|
|
137
|
+
if _dq_flags:
|
|
138
|
+
_lines += [
|
|
139
|
+
"",
|
|
140
|
+
"## Data Quality Flags",
|
|
141
|
+
"",
|
|
142
|
+
"| Column | Flag | Detail |",
|
|
143
|
+
"|--------|------|--------|",
|
|
144
|
+
]
|
|
145
|
+
for _dq_col, _dq_flag, _dq_detail in _dq_flags:
|
|
146
|
+
_lines.append(f"| `{{_dq_col}}` | {{_dq_flag}} | {{_dq_detail}} |")
|
|
147
|
+
|
|
148
|
+
# --- Numeric summary ---
|
|
149
|
+
_num_cols = _df.select_dtypes(include="number").columns.tolist()
|
|
150
|
+
if _num_cols:
|
|
151
|
+
_lines += [
|
|
152
|
+
"",
|
|
153
|
+
"## Numeric Summary",
|
|
154
|
+
"",
|
|
155
|
+
"| Column | Mean | Std | Min | p25 | p50 | p75 | Max |",
|
|
156
|
+
"|--------|------|-----|-----|-----|-----|-----|-----|",
|
|
157
|
+
]
|
|
158
|
+
_desc = _df[_num_cols].describe(percentiles=[0.25, 0.5, 0.75])
|
|
159
|
+
|
|
160
|
+
def _fmt(v):
|
|
161
|
+
if pd.isna(v):
|
|
162
|
+
return "—"
|
|
163
|
+
return f"{{v:,.2f}}" if abs(v) < 1_000_000 else f"{{v:.3e}}"
|
|
164
|
+
|
|
165
|
+
for _col in _num_cols:
|
|
166
|
+
_lines.append(
|
|
167
|
+
f"| `{{_col}}` "
|
|
168
|
+
f"| {{_fmt(_desc.loc['mean', _col])}} "
|
|
169
|
+
f"| {{_fmt(_desc.loc['std', _col])}} "
|
|
170
|
+
f"| {{_fmt(_desc.loc['min', _col])}} "
|
|
171
|
+
f"| {{_fmt(_desc.loc['25%', _col])}} "
|
|
172
|
+
f"| {{_fmt(_desc.loc['50%', _col])}} "
|
|
173
|
+
f"| {{_fmt(_desc.loc['75%', _col])}} "
|
|
174
|
+
f"| {{_fmt(_desc.loc['max', _col])}} |"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# --- Top categoricals ---
|
|
178
|
+
try:
|
|
179
|
+
_cat_dtypes = ["object", "category", "string"]
|
|
180
|
+
_cat_cols = _df.select_dtypes(include=_cat_dtypes).columns.tolist()
|
|
181
|
+
except Exception:
|
|
182
|
+
_cat_cols = _df.select_dtypes(include=["object", "category"]).columns.tolist()
|
|
183
|
+
|
|
184
|
+
if _cat_cols:
|
|
185
|
+
_lines.append("")
|
|
186
|
+
_lines.append("## Top Categoricals")
|
|
187
|
+
for _col in _cat_cols:
|
|
188
|
+
_vc = _df[_col].value_counts().head(5)
|
|
189
|
+
if _vc.empty:
|
|
190
|
+
continue
|
|
191
|
+
_lines += [
|
|
192
|
+
"",
|
|
193
|
+
f"### `{{_col}}`",
|
|
194
|
+
"",
|
|
195
|
+
"| Value | Count | % |",
|
|
196
|
+
"|-------|-------|---|",
|
|
197
|
+
]
|
|
198
|
+
for _val, _cnt in _vc.items():
|
|
199
|
+
_pct = (_cnt / _rows * 100) if _rows > 0 else 0.0
|
|
200
|
+
_display = str(_val)
|
|
201
|
+
if len(_display) > 40:
|
|
202
|
+
_display = _display[:37] + "..."
|
|
203
|
+
_lines.append(f"| {{_display!r}} | {{_cnt:,}} | {{_pct:.1f}}% |")
|
|
204
|
+
|
|
205
|
+
# --- Pandera inferred schema (dtype + nullable per column) ---
|
|
206
|
+
if _pa_schema is not None:
|
|
207
|
+
_lines += [
|
|
208
|
+
"",
|
|
209
|
+
"## Inferred Schema (pandera)",
|
|
210
|
+
"",
|
|
211
|
+
"| Column | Inferred dtype | Nullable |",
|
|
212
|
+
"|--------|----------------|----------|",
|
|
213
|
+
]
|
|
214
|
+
for _pa_col_name, _pa_col in _pa_schema.columns.items():
|
|
215
|
+
_pa_col_nullable = "yes" if _pa_col.nullable else "no"
|
|
216
|
+
_lines.append(
|
|
217
|
+
f"| `{{_pa_col_name}}` | {{_pa_col.dtype}} | {{_pa_col_nullable}} |"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
result = "\\n".join(_lines) + "\\n"
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def build_profile_code(path: str) -> str:
|
|
225
|
+
"""Return a sandbox-ready Python snippet that profiles the dataset at *path*.
|
|
226
|
+
|
|
227
|
+
The snippet sets ``result`` to the Markdown profile card on success, or to
|
|
228
|
+
a ``__PROFILE_SKIP__<reason>`` sentinel string when the file cannot be
|
|
229
|
+
loaded (so the caller can surface a helpful message without saving garbage).
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
path: Absolute filesystem path to the dataset file. Must be absolute
|
|
233
|
+
so the snippet runs correctly regardless of the sandbox CWD.
|
|
234
|
+
"""
|
|
235
|
+
return _PROFILE_CODE_TEMPLATE.format(path_repr=repr(path))
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# ---------------------------------------------------------------------------
|
|
239
|
+
# LangChain tools
|
|
240
|
+
# ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def create_read_dataset_info_tools(context: BaseContextStore | None) -> list[BaseTool]:
|
|
244
|
+
"""Return the ``read_dataset_info`` tool bound to *context*."""
|
|
245
|
+
|
|
246
|
+
@tool
|
|
247
|
+
async def read_dataset_info(path: str, summary: str, communication: str) -> str:
|
|
248
|
+
"""Load all accumulated knowledge about a dataset — profile card and agent notes.
|
|
249
|
+
|
|
250
|
+
Returns a Markdown string with two sections:
|
|
251
|
+
- ``# DATASET PROFILING`` — auto-generated profile card (shape, dtypes, null rates,
|
|
252
|
+
numeric summary, top categoricals).
|
|
253
|
+
- ``# DATASET NOTES`` — cumulative agent notes from all prior sessions. Primary source
|
|
254
|
+
of institutional knowledge; read carefully before exploring.
|
|
255
|
+
|
|
256
|
+
# When to use this tool
|
|
257
|
+
- Always, before writing any code that reads or processes a dataset.
|
|
258
|
+
- Even if you profiled the dataset earlier in the session — notes may have been
|
|
259
|
+
updated by ``update_dataset_info`` since then.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
path: Absolute or relative path to the dataset file or directory.
|
|
263
|
+
summary: 3-4 word status label (e.g. "Reading train.csv info").
|
|
264
|
+
communication: Brief message to the user about what you're doing
|
|
265
|
+
(e.g. "Let me read existing notes about the dataset.").
|
|
266
|
+
"""
|
|
267
|
+
if context is None:
|
|
268
|
+
return "Error: No workspace path available."
|
|
269
|
+
try:
|
|
270
|
+
return await context.read_dataset_info(path)
|
|
271
|
+
except FileNotFoundError as exc:
|
|
272
|
+
return f"Error: {exc}"
|
|
273
|
+
except Exception as exc:
|
|
274
|
+
return f"Error loading dataset info: {type(exc).__name__}: {exc}"
|
|
275
|
+
|
|
276
|
+
return [read_dataset_info]
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def create_profile_dataset_tools(
|
|
280
|
+
context: BaseContextStore | None, sandbox: BaseSandbox, persist: bool = True
|
|
281
|
+
) -> list[BaseTool]:
|
|
282
|
+
"""Return the ``profile_dataset`` tool bound to *context* and *sandbox*."""
|
|
283
|
+
|
|
284
|
+
@tool
|
|
285
|
+
async def profile_dataset(path: str, summary: str, communication: str) -> str:
|
|
286
|
+
"""Auto-profile a dataset and return its card (shape, dtypes, null rates, distributions).
|
|
287
|
+
|
|
288
|
+
Profiles are cached — subsequent calls return the existing card without re-scanning.
|
|
289
|
+
The card covers: shape, dtypes, null rates, numeric summary, top categoricals,
|
|
290
|
+
data-quality flags, and inferred schema.
|
|
291
|
+
|
|
292
|
+
# When to use this tool
|
|
293
|
+
- Once per new dataset, before exploring it.
|
|
294
|
+
- When ``read_dataset_info`` returns an empty or stale profile for a dataset.
|
|
295
|
+
|
|
296
|
+
# When NOT to use this tool
|
|
297
|
+
- When a profile was already returned by ``read_dataset_info`` this session — it is current.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
path: Absolute or relative path to the dataset file or directory.
|
|
301
|
+
summary: 3-4 word status label (e.g. "Profiling train.csv").
|
|
302
|
+
communication: Brief message to the user about what you're doing
|
|
303
|
+
(e.g. "Let me profile the dataset to understand its structure.").
|
|
304
|
+
"""
|
|
305
|
+
if context is None:
|
|
306
|
+
return "Error: No workspace path available."
|
|
307
|
+
try:
|
|
308
|
+
wc = context
|
|
309
|
+
resolved, hash_hex, existing = await wc.get_profile_info(path)
|
|
310
|
+
|
|
311
|
+
if existing is not None:
|
|
312
|
+
return existing
|
|
313
|
+
|
|
314
|
+
code = build_profile_code(resolved)
|
|
315
|
+
exec_result = await sandbox.execute(code)
|
|
316
|
+
|
|
317
|
+
if not exec_result.success:
|
|
318
|
+
return f"Profiling failed: {exec_result.error}"
|
|
319
|
+
|
|
320
|
+
if exec_result.output is None:
|
|
321
|
+
return "Profiling produced no output — the dataset may be empty."
|
|
322
|
+
|
|
323
|
+
content = str(exec_result.output)
|
|
324
|
+
if content.startswith("__PROFILE_SKIP__"):
|
|
325
|
+
return content[len("__PROFILE_SKIP__") :]
|
|
326
|
+
|
|
327
|
+
if persist:
|
|
328
|
+
wc.save_dataset_profile(hash_hex, content)
|
|
329
|
+
return content
|
|
330
|
+
|
|
331
|
+
except FileNotFoundError as exc:
|
|
332
|
+
return f"Error: {exc}"
|
|
333
|
+
except Exception as exc:
|
|
334
|
+
return f"Error profiling dataset: {type(exc).__name__}: {exc}"
|
|
335
|
+
|
|
336
|
+
return [profile_dataset]
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def create_update_dataset_info_tools(context: BaseContextStore | None) -> list[BaseTool]:
|
|
340
|
+
"""Return the ``update_dataset_info`` tool bound to *context*."""
|
|
341
|
+
|
|
342
|
+
@tool
|
|
343
|
+
async def update_dataset_info(path: str, update: str, merge: bool = False) -> str:
|
|
344
|
+
"""Persist findings, observations, and decisions for a dataset across sessions.
|
|
345
|
+
|
|
346
|
+
This is how knowledge carries forward — everything written here is surfaced
|
|
347
|
+
automatically next session via ``read_dataset_info``. Skipping means the
|
|
348
|
+
next session starts blind to the new knowledge and context about you've gathered
|
|
349
|
+
about the data and tasks you've tackled around it.
|
|
350
|
+
|
|
351
|
+
# When to use this tool
|
|
352
|
+
- After every turn that touches a dataset: findings, quality issues, surprises,
|
|
353
|
+
failed approaches, user decisions, and hypotheses to revisit.
|
|
354
|
+
- Err on the side of capturing more — even minor or confirmatory findings.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
path: Absolute or relative path to the dataset file or directory.
|
|
358
|
+
update: Markdown content to write or append. Make it structured.
|
|
359
|
+
merge: ``True`` to append to existing notes (default);
|
|
360
|
+
``False`` to overwrite all prior notes.
|
|
361
|
+
"""
|
|
362
|
+
if context is None:
|
|
363
|
+
return "Error: No workspace path available."
|
|
364
|
+
try:
|
|
365
|
+
return await context.update_dataset_info(path, update, merge=merge)
|
|
366
|
+
except FileNotFoundError as exc:
|
|
367
|
+
return f"Error: {exc}"
|
|
368
|
+
except Exception as exc:
|
|
369
|
+
return f"Error updating dataset info: {type(exc).__name__}: {exc}"
|
|
370
|
+
|
|
371
|
+
return [update_dataset_info]
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def create_data_context_tools(
|
|
375
|
+
context: BaseContextStore | None, sandbox: BaseSandbox, persist: bool = True
|
|
376
|
+
) -> list[BaseTool]:
|
|
377
|
+
"""Return dataset-context tools bound to *context* and *sandbox*.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
context: I/O boundary for dataset notes and profiles.
|
|
381
|
+
sandbox: Execution sandbox used by ``profile_dataset``.
|
|
382
|
+
persist: When ``False``, ``update_dataset_info`` is excluded and
|
|
383
|
+
``profile_dataset`` will not write profiles to disk.
|
|
384
|
+
"""
|
|
385
|
+
tools = [
|
|
386
|
+
*create_read_dataset_info_tools(context),
|
|
387
|
+
*create_profile_dataset_tools(context, sandbox, persist=persist),
|
|
388
|
+
]
|
|
389
|
+
if persist:
|
|
390
|
+
tools.extend(create_update_dataset_info_tools(context))
|
|
391
|
+
return tools
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Tool factories: assemble the right tool sets for main and worker agents."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from langchain_core.tools import BaseTool
|
|
9
|
+
|
|
10
|
+
from opendatasci.context.base import BaseContextStore
|
|
11
|
+
from opendatasci.sandbox.base import BaseSandbox, BaseSandboxFactory
|
|
12
|
+
from opendatasci.skills import BaseSkillStore
|
|
13
|
+
from opendatasci.skills.local import LocalSkillStore
|
|
14
|
+
from opendatasci.tools.coding import (
|
|
15
|
+
create_cli_tools,
|
|
16
|
+
create_code_verification_tools,
|
|
17
|
+
create_coding_tools,
|
|
18
|
+
)
|
|
19
|
+
from opendatasci.tools.critic import create_critic_tools
|
|
20
|
+
from opendatasci.tools.dataset_info import create_data_context_tools
|
|
21
|
+
from opendatasci.tools.mcp import create_mcp_tools
|
|
22
|
+
from opendatasci.tools.planning import create_planning_tools
|
|
23
|
+
from opendatasci.tools.skills import create_skill_tools
|
|
24
|
+
from opendatasci.tools.user_interaction import create_user_interaction_tools
|
|
25
|
+
from opendatasci.tools.web import create_web_tools
|
|
26
|
+
from opendatasci.tools.workers import create_worker_tools
|
|
27
|
+
from opendatasci.tools.workspace import create_workspace_tools
|
|
28
|
+
from opendatasci.workspace.base import BaseWorkspace
|
|
29
|
+
from opendatasci.workspace.local import LocalWorkspace
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from opendatasci.configs import OpenDataSciConfig
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ToolName(str, Enum):
|
|
36
|
+
"""Canonical names for all agent tools."""
|
|
37
|
+
|
|
38
|
+
EXECUTE_PYTHON_CODE = "execute_python_code"
|
|
39
|
+
EXECUTE_CLI = "execute_cli_command"
|
|
40
|
+
LIST_PYTHON_LIBS = "list_python_libs"
|
|
41
|
+
LOAD_SKILL = "load_skill"
|
|
42
|
+
ENTER_PLAN_MODE = "enter_plan_mode"
|
|
43
|
+
EXIT_PLAN_MODE = "exit_plan_mode"
|
|
44
|
+
ENTER_SELF_REVIEW_MODE = "enter_self_review_mode"
|
|
45
|
+
EXIT_SELF_REVIEW_MODE = "exit_self_review_mode"
|
|
46
|
+
SPAWN_WORKERS = "spawn_workers"
|
|
47
|
+
READ_DATASET_INFO = "read_dataset_info"
|
|
48
|
+
UPDATE_DATASET_INFO = "update_dataset_info"
|
|
49
|
+
PROFILE_DATASET = "profile_dataset"
|
|
50
|
+
LIST_WORKSPACE_FILES = "list_workspace_files"
|
|
51
|
+
WEB_SEARCH = "web_search"
|
|
52
|
+
FETCH_URL = "fetch_url"
|
|
53
|
+
ASK_USER_MCQ = "ask_user_mcq"
|
|
54
|
+
VERIFY_PYTHON_CODE = "verify_python_code"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _base_tools(
|
|
58
|
+
workspace: BaseWorkspace,
|
|
59
|
+
sandbox: BaseSandbox,
|
|
60
|
+
context: "BaseContextStore | None",
|
|
61
|
+
store: BaseSkillStore,
|
|
62
|
+
persist: bool = True,
|
|
63
|
+
) -> list[BaseTool]:
|
|
64
|
+
"""Return the tools shared by both main and worker agents.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
workspace: Workspace container.
|
|
68
|
+
sandbox: Code execution sandbox.
|
|
69
|
+
context: I/O boundary for dataset notes and profiles.
|
|
70
|
+
store: Skill store used by the ``load_skill`` tool.
|
|
71
|
+
persist: When ``False``, write-side tools (``update_dataset_info``)
|
|
72
|
+
are excluded and ``profile_dataset`` will not write profiles
|
|
73
|
+
to disk.
|
|
74
|
+
"""
|
|
75
|
+
tools: list[BaseTool] = [
|
|
76
|
+
*create_coding_tools(sandbox),
|
|
77
|
+
*create_cli_tools(sandbox),
|
|
78
|
+
*create_data_context_tools(context, sandbox, persist=persist),
|
|
79
|
+
*create_skill_tools(store),
|
|
80
|
+
]
|
|
81
|
+
if isinstance(workspace, LocalWorkspace):
|
|
82
|
+
tools.extend(create_workspace_tools(Path(workspace.get_reference())))
|
|
83
|
+
return tools
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def create_worker_agent_tools(
|
|
87
|
+
workspace: BaseWorkspace,
|
|
88
|
+
context: "BaseContextStore | None",
|
|
89
|
+
sandbox: BaseSandbox | None = None,
|
|
90
|
+
store: BaseSkillStore | None = None,
|
|
91
|
+
) -> list[BaseTool]:
|
|
92
|
+
"""Return the tool list for a worker agent.
|
|
93
|
+
|
|
94
|
+
Workers share the same core tools as the main agent but cannot spawn
|
|
95
|
+
further workers, plan, or access the web.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
workspace: Workspace container.
|
|
99
|
+
context: I/O boundary for dataset notes and profiles.
|
|
100
|
+
sandbox: Code execution sandbox. A new :class:`~opendatasci.sandbox.srt.SRTSandbox`
|
|
101
|
+
is created when ``None``.
|
|
102
|
+
store: Skill store injected from the caller. Defaults to a
|
|
103
|
+
:class:`~opendatasci.skills.local.LocalSkillStore` rooted
|
|
104
|
+
at ``<context.root>/skills``.
|
|
105
|
+
"""
|
|
106
|
+
if sandbox is None:
|
|
107
|
+
from opendatasci.sandbox.srt import SRTSandbox
|
|
108
|
+
|
|
109
|
+
sandbox = SRTSandbox(workspace_path=Path(workspace.get_reference()))
|
|
110
|
+
if store is None:
|
|
111
|
+
user_skills_dir = Path(context.root) / "skills" if context is not None else None
|
|
112
|
+
store = LocalSkillStore([user_skills_dir] if user_skills_dir is not None else None)
|
|
113
|
+
return _base_tools(workspace, sandbox, context, store, persist=False)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def create_agent_tools(
|
|
117
|
+
workspace: BaseWorkspace,
|
|
118
|
+
sandbox: BaseSandbox,
|
|
119
|
+
context: "BaseContextStore | None",
|
|
120
|
+
sandbox_factory: BaseSandboxFactory,
|
|
121
|
+
store: BaseSkillStore | None = None,
|
|
122
|
+
datasci_config: "OpenDataSciConfig | None" = None,
|
|
123
|
+
save_plan: "Callable[[str], None] | None" = None,
|
|
124
|
+
) -> list[BaseTool]:
|
|
125
|
+
"""Return the tool list for the main agent.
|
|
126
|
+
|
|
127
|
+
Extends the worker tool set with planning, worker spawning, web access,
|
|
128
|
+
and user interaction.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
workspace: Workspace container.
|
|
132
|
+
sandbox: Code execution sandbox.
|
|
133
|
+
context: I/O boundary for dataset notes and profiles.
|
|
134
|
+
sandbox_factory: Factory used by spawned workers to create their own
|
|
135
|
+
isolated sandboxes.
|
|
136
|
+
store: Skill store injected from the caller. Defaults to a
|
|
137
|
+
:class:`~opendatasci.skills.local.LocalSkillStore` rooted
|
|
138
|
+
at ``<context.root>/skills``.
|
|
139
|
+
datasci_config: LLM configuration forwarded to spawned workers.
|
|
140
|
+
save_plan: Callback that persists the final plan via
|
|
141
|
+
``BaseContextStore``. When provided,
|
|
142
|
+
``enter_plan_mode`` and ``exit_plan_mode`` are added
|
|
143
|
+
to the tool list.
|
|
144
|
+
"""
|
|
145
|
+
if store is None:
|
|
146
|
+
user_skills_dir = Path(context.root) / "skills" if context is not None else None
|
|
147
|
+
store = LocalSkillStore([user_skills_dir] if user_skills_dir is not None else None)
|
|
148
|
+
tools = _base_tools(workspace, sandbox, context, store)
|
|
149
|
+
if datasci_config is not None:
|
|
150
|
+
tools.extend(create_code_verification_tools(datasci_config))
|
|
151
|
+
if save_plan is not None:
|
|
152
|
+
tools.extend(create_planning_tools(save_plan))
|
|
153
|
+
tools.extend(create_critic_tools(store))
|
|
154
|
+
tools.extend(
|
|
155
|
+
create_worker_tools(
|
|
156
|
+
workspace,
|
|
157
|
+
context,
|
|
158
|
+
datasci_config,
|
|
159
|
+
store=store,
|
|
160
|
+
sandbox_factory=sandbox_factory,
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
tools.extend(
|
|
164
|
+
create_web_tools(
|
|
165
|
+
datasci_config.extra_web_domains if datasci_config else (),
|
|
166
|
+
datasci_config.override_web_domains if datasci_config else None,
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
tools.extend(create_user_interaction_tools())
|
|
170
|
+
if datasci_config is not None and datasci_config.mcp_servers:
|
|
171
|
+
tools.extend(create_mcp_tools(datasci_config.mcp_servers))
|
|
172
|
+
return tools
|