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.
Files changed (85) hide show
  1. open_data_sci-0.1.0.dist-info/METADATA +629 -0
  2. open_data_sci-0.1.0.dist-info/RECORD +85 -0
  3. open_data_sci-0.1.0.dist-info/WHEEL +4 -0
  4. open_data_sci-0.1.0.dist-info/entry_points.txt +2 -0
  5. open_data_sci-0.1.0.dist-info/licenses/LICENSE +201 -0
  6. opendatasci/__init__.py +47 -0
  7. opendatasci/_tui/__init__.py +1 -0
  8. opendatasci/_tui/adapter.py +102 -0
  9. opendatasci/_tui/app.py +429 -0
  10. opendatasci/_tui/commands.py +95 -0
  11. opendatasci/_tui/completion.py +139 -0
  12. opendatasci/_tui/controller.py +644 -0
  13. opendatasci/_tui/file_refs.py +153 -0
  14. opendatasci/_tui/models.py +4 -0
  15. opendatasci/_tui/presenter.py +259 -0
  16. opendatasci/_tui/service.py +78 -0
  17. opendatasci/_tui/session.py +53 -0
  18. opendatasci/_tui/styles.tcss +248 -0
  19. opendatasci/_tui/styles_visible.tcss +245 -0
  20. opendatasci/_tui/theme.py +113 -0
  21. opendatasci/_tui/tools_display.py +86 -0
  22. opendatasci/_tui/widgets.py +1001 -0
  23. opendatasci/_utils/__init__.py +0 -0
  24. opendatasci/_utils/async_utils.py +11 -0
  25. opendatasci/_utils/data_formats.py +135 -0
  26. opendatasci/_utils/hash_utils.py +52 -0
  27. opendatasci/_utils/langchain_utils.py +155 -0
  28. opendatasci/_utils/streaming_utils.py +23 -0
  29. opendatasci/agents/__init__.py +12 -0
  30. opendatasci/agents/agents.py +515 -0
  31. opendatasci/agents/agents_factory.py +71 -0
  32. opendatasci/agents/chat_memory.py +397 -0
  33. opendatasci/agents/graphs.py +84 -0
  34. opendatasci/agents/nodes.py +74 -0
  35. opendatasci/agents/states.py +36 -0
  36. opendatasci/agents/turn_memory.py +124 -0
  37. opendatasci/configs.py +275 -0
  38. opendatasci/context/__init__.py +7 -0
  39. opendatasci/context/base.py +56 -0
  40. opendatasci/context/local.py +236 -0
  41. opendatasci/models/__init__.py +7 -0
  42. opendatasci/models/anthropic.py +40 -0
  43. opendatasci/models/aws.py +86 -0
  44. opendatasci/models/factory.py +179 -0
  45. opendatasci/models/google.py +79 -0
  46. opendatasci/models/local.py +79 -0
  47. opendatasci/models/microsoft.py +62 -0
  48. opendatasci/models/openai.py +49 -0
  49. opendatasci/models/providers.py +12 -0
  50. opendatasci/prompts/__init__.py +5 -0
  51. opendatasci/prompts/builders.py +85 -0
  52. opendatasci/prompts/caching.py +42 -0
  53. opendatasci/prompts/message_templates.py +7 -0
  54. opendatasci/prompts/prompt_templates.py +227 -0
  55. opendatasci/resources/skills/competitive_data_science.md +241 -0
  56. opendatasci/resources/skills/data_science.md +55 -0
  57. opendatasci/resources/skills/data_science_education.md +42 -0
  58. opendatasci/resources/skills/deep_learning.md +205 -0
  59. opendatasci/resources/skills/machine_learning.md +68 -0
  60. opendatasci/resources/skills/quantitative_analysis.md +45 -0
  61. opendatasci/sandbox/__init__.py +14 -0
  62. opendatasci/sandbox/_runner.py +114 -0
  63. opendatasci/sandbox/base.py +170 -0
  64. opendatasci/sandbox/srt.py +490 -0
  65. opendatasci/skills/__init__.py +9 -0
  66. opendatasci/skills/base.py +28 -0
  67. opendatasci/skills/local.py +131 -0
  68. opendatasci/streaming/__init__.py +37 -0
  69. opendatasci/streaming/events.py +159 -0
  70. opendatasci/streaming/processors.py +387 -0
  71. opendatasci/tools/__init__.py +58 -0
  72. opendatasci/tools/coding.py +261 -0
  73. opendatasci/tools/critic.py +136 -0
  74. opendatasci/tools/dataset_info.py +391 -0
  75. opendatasci/tools/factory.py +172 -0
  76. opendatasci/tools/mcp.py +179 -0
  77. opendatasci/tools/planning.py +88 -0
  78. opendatasci/tools/skills.py +90 -0
  79. opendatasci/tools/user_interaction.py +54 -0
  80. opendatasci/tools/web.py +236 -0
  81. opendatasci/tools/workers.py +237 -0
  82. opendatasci/tools/workspace.py +55 -0
  83. opendatasci/workspace/__init__.py +9 -0
  84. opendatasci/workspace/base.py +20 -0
  85. 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