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,237 @@
1
+ """ConcurrentWorkerAgent spawning tool: spawn_workers."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, Annotated, Any
7
+
8
+ from annotated_types import MaxLen, MinLen
9
+ from langchain_core.callbacks.manager import adispatch_custom_event
10
+ from langchain_core.runnables.config import RunnableConfig, ensure_config
11
+ from langchain_core.tools import BaseTool, tool
12
+ from pydantic import BaseModel
13
+
14
+ from opendatasci.context.base import BaseContextStore
15
+ from opendatasci.prompts.prompt_templates import WORKER_SYSTEM_PROMPT
16
+ from opendatasci.sandbox.base import BaseSandboxFactory
17
+ from opendatasci.skills import BaseSkillStore
18
+ from opendatasci.skills.local import LocalSkillStore
19
+ from opendatasci.tools.coding import create_cli_tools, create_coding_tools
20
+ from opendatasci.tools.skills import create_skill_tools
21
+ from opendatasci.tools.web import create_web_tools
22
+ from opendatasci.workspace.base import BaseWorkspace
23
+
24
+ if TYPE_CHECKING:
25
+ from opendatasci.configs import OpenDataSciConfig
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ async def _run_one(
31
+ idx: int,
32
+ subtask: "WorkerTask",
33
+ outer_config: RunnableConfig,
34
+ *,
35
+ sandbox_factory: BaseSandboxFactory,
36
+ workspace: BaseWorkspace,
37
+ store: BaseSkillStore,
38
+ datasci_config: "OpenDataSciConfig | None",
39
+ ) -> str:
40
+ """Run a single worker subtask inside its own sandbox.
41
+
42
+ Args:
43
+ idx: Zero-based worker index used to tag emitted events.
44
+ subtask: Subtask descriptor including instructions and options.
45
+ outer_config: LangChain config from the calling graph, captured before
46
+ any inner graph run can overwrite the context var — ensures
47
+ ``adispatch_custom_event`` always targets the right callback
48
+ chain regardless of which async context is active at fire time.
49
+ sandbox_factory: Factory used to create the worker's isolated sandbox.
50
+ workspace: Workspace the worker operates on.
51
+ store: Skill store to resolve ``subtask.skill``.
52
+ datasci_config: LLM configuration forwarded to the worker agent.
53
+ """
54
+ initial_skill = None
55
+ if subtask.skill is not None:
56
+ initial_skill = store.load(subtask.skill)
57
+ if initial_skill is None:
58
+ logger.warning(
59
+ "Worker %d: requested skill %r is unknown; starting without a preloaded skill.",
60
+ idx,
61
+ subtask.skill,
62
+ )
63
+
64
+ def emit(event_type: str, content: str, metadata: dict[str, Any] | None = None) -> None:
65
+ asyncio.get_running_loop().create_task(
66
+ adispatch_custom_event(
67
+ "worker_event",
68
+ {
69
+ "worker_idx": idx,
70
+ "event_type": event_type,
71
+ "content": content,
72
+ **(metadata or {}),
73
+ },
74
+ config=outer_config,
75
+ )
76
+ )
77
+
78
+ cancelled = False
79
+ exc_info: BaseException | None = None
80
+
81
+ async with sandbox_factory.create(
82
+ workspace_path=Path(workspace.get_reference())
83
+ ) as worker_sandbox:
84
+ tools: list[BaseTool] = [
85
+ *create_coding_tools(worker_sandbox),
86
+ *create_cli_tools(worker_sandbox),
87
+ *create_skill_tools(store),
88
+ ]
89
+ if subtask.allow_web_tools:
90
+ tools.extend(
91
+ create_web_tools(
92
+ datasci_config.extra_web_domains if datasci_config is not None else (),
93
+ datasci_config.override_web_domains if datasci_config is not None else None,
94
+ )
95
+ )
96
+ from opendatasci.agents.agents import (
97
+ ConcurrentWorkerAgent,
98
+ ) # local import breaks circular dependency
99
+
100
+ agent = ConcurrentWorkerAgent(tools=tools, config=datasci_config)
101
+ emit("worker_started", subtask.summary)
102
+
103
+ try:
104
+ return await agent.ainvoke(
105
+ subtask.subtask,
106
+ WORKER_SYSTEM_PROMPT,
107
+ on_event=emit,
108
+ initial_active_skills=[initial_skill] if initial_skill is not None else [],
109
+ )
110
+ except asyncio.CancelledError:
111
+ cancelled = True
112
+ raise
113
+ except RuntimeError as exc:
114
+ exc_info = exc
115
+ return str(exc)
116
+ except Exception as exc:
117
+ exc_info = exc
118
+ raise
119
+ finally:
120
+ if not cancelled:
121
+ success = exc_info is None
122
+ emit("worker_finished", subtask.summary, {"success": success})
123
+ emit("worker_done", subtask.summary, {"success": success})
124
+
125
+
126
+ class WorkerTask(BaseModel):
127
+ """A subtask descriptor for a worker."""
128
+
129
+ subtask: str
130
+ """Specific, self-contained subtask with all context the worker needs."""
131
+ summary: str
132
+ """3-4 word status label (e.g. ``'Shapiro-Wilk on age'``)."""
133
+ skill: str | None = None
134
+ """Optional skill profile to preload before the subtask runs
135
+ (e.g. ``'data_science'``, ``'ml_engineering'``). ``None`` = no skill."""
136
+ allow_web_tools: bool = False
137
+ """When ``True``, the worker can use ``web_search`` and ``fetch_url``
138
+ to look up documentation, papers, or API references."""
139
+
140
+
141
+ def create_worker_tools(
142
+ workspace: BaseWorkspace,
143
+ context: "BaseContextStore | None",
144
+ datasci_config: "OpenDataSciConfig | None",
145
+ sandbox_factory: BaseSandboxFactory,
146
+ store: BaseSkillStore | None = None,
147
+ ) -> list[BaseTool]:
148
+ """Return the spawn_workers tool.
149
+
150
+ Each spawned worker receives its own isolated sandbox created through
151
+ *sandbox_factory* so that teardown is guaranteed on completion or error.
152
+ Worker lifecycle events are dispatched directly into the calling graph's
153
+ event stream via :func:`langchain_core.callbacks.manager.adispatch_custom_event`
154
+ under the name ``"worker_event"``, eliminating the need for side-channel queues.
155
+
156
+ Args:
157
+ workspace: Workspace the workers operate on.
158
+ context: Work context from the main agent; used to resolve the
159
+ skills directory.
160
+ datasci_config: LLM configuration forwarded to each worker.
161
+ sandbox_factory: Factory used to create an isolated sandbox for each worker.
162
+ store: Skill store shared across all spawned workers. Defaults
163
+ to a :class:`~opendatasci.skills.local.LocalSkillStore`
164
+ rooted at ``<context.root>/skills``.
165
+ """
166
+ if store is None:
167
+ user_skills_dir = Path(context.root) / "skills" if context is not None else None
168
+ store = LocalSkillStore([user_skills_dir] if user_skills_dir is not None else None)
169
+
170
+ @tool
171
+ async def spawn_workers(
172
+ subtasks: Annotated[list[WorkerTask], MinLen(1), MaxLen(3)],
173
+ communication: str,
174
+ ) -> str:
175
+ """Spawn 1–3 independent workers to execute narrow, concrete subtasks in parallel.
176
+
177
+ Workers are fully isolated: no shared state, no conversation history, no context from
178
+ other subtasks. Each subtask runs to completion independently before results are collected.
179
+
180
+ # When to use this tool
181
+ - For specific, orthogonal actions with a clearly defined outcome that can run concurrently:
182
+ e.g. "Run Shapiro-Wilk on `age`", "Investigate the distribution of `revenue`".
183
+ - When the task has already been planned and workers execute individual, independent steps.
184
+
185
+ # When NOT to use this tool
186
+ - When one subtask's result informs another — workers cannot pass data to each other.
187
+ - For broad exploration or re-planning — workers execute, they don't strategise.
188
+ - For a single task: just execute directly; one worker adds latency with no benefit.
189
+
190
+ # How to use this tool
191
+ - Write every subtask description as fully self-contained: include dataset names,
192
+ variable names, target columns, and any context the worker needs from the conversation.
193
+ - Assign a ``skill`` when the subtask benefits from domain-specific guidance.
194
+
195
+ Args:
196
+ subtasks: 1–3 subtask descriptors (see WorkerTask fields).
197
+ communication: Brief message to the user about what you're doing
198
+ (e.g. "Running three checks in parallel.").
199
+ """
200
+ outer_config = ensure_config()
201
+ timeout = datasci_config.worker_timeout_seconds if datasci_config is not None else 300.0
202
+ results = await asyncio.wait_for(
203
+ asyncio.gather(
204
+ *[
205
+ _run_one(
206
+ i,
207
+ t,
208
+ outer_config,
209
+ sandbox_factory=sandbox_factory,
210
+ workspace=workspace,
211
+ store=store,
212
+ datasci_config=datasci_config,
213
+ )
214
+ for i, t in enumerate(subtasks)
215
+ ],
216
+ return_exceptions=True,
217
+ ),
218
+ timeout=timeout,
219
+ )
220
+
221
+ sections: list[str] = []
222
+ for i, (subtask, result) in enumerate(zip(subtasks, results), 1):
223
+ if isinstance(result, BaseException):
224
+ logger.error(
225
+ "Worker %d (%s) failed: %s: %s",
226
+ i,
227
+ subtask.summary,
228
+ type(result).__name__,
229
+ result,
230
+ )
231
+ output = f"Error: worker failed — {type(result).__name__}: {result}"
232
+ else:
233
+ output = result
234
+ sections.append(f"### ConcurrentWorkerAgent {i}: {subtask.subtask}\n\n{output}")
235
+ return "\n\n---\n\n".join(sections)
236
+
237
+ return [spawn_workers]
@@ -0,0 +1,55 @@
1
+ """Workspace navigation tool: inspect workspace files."""
2
+
3
+ from pathlib import Path
4
+
5
+ from langchain_core.tools import BaseTool, tool
6
+
7
+
8
+ def create_workspace_tools(workspace_path: Path | None) -> list[BaseTool]:
9
+ """Return workspace tools bound to *workspace_path*."""
10
+
11
+ @tool
12
+ def list_workspace_files(summary: str, communication: str) -> str:
13
+ """Map the active workspace: list all files and directories with sizes.
14
+
15
+ Call when you need to know what files are available in the workspace,
16
+ and before referencing workspace paths in code.
17
+ Hidden directories (dot-prefixed) are excluded.
18
+
19
+ Args:
20
+ summary: 3-4 word status label (e.g. "Listing workspace files").
21
+ communication: Brief message to the user about what you're doing
22
+ (e.g. "Let me check what files are available.").
23
+ """
24
+ path = workspace_path
25
+ if path is None:
26
+ return "No active workspace."
27
+ try:
28
+ entries = sorted(
29
+ (
30
+ f
31
+ for f in path.rglob("*")
32
+ if not any(part.startswith(".") for part in f.relative_to(path).parts)
33
+ ),
34
+ key=lambda f: (f.is_dir(), str(f).lower()),
35
+ )
36
+ if not entries:
37
+ return f"Workspace '{path.name}' is empty."
38
+ lines = [f"Files in workspace '{path.name}':"]
39
+ for entry in entries:
40
+ if entry.is_dir():
41
+ lines.append(f" {entry}/")
42
+ else:
43
+ size = entry.stat().st_size
44
+ if size < 1024:
45
+ size_str = f"{size} B"
46
+ elif size < 1024**2:
47
+ size_str = f"{size / 1024:.1f} KB"
48
+ else:
49
+ size_str = f"{size / 1024**2:.1f} MB"
50
+ lines.append(f" {entry} ({size_str})")
51
+ return "\n".join(lines)
52
+ except Exception as exc:
53
+ return f"Error listing workspace files: {type(exc).__name__}: {exc}"
54
+
55
+ return [list_workspace_files]
@@ -0,0 +1,9 @@
1
+ """Workspace — abstractions and local implementation."""
2
+
3
+ from opendatasci.workspace.base import BaseWorkspace
4
+ from opendatasci.workspace.local import LocalWorkspace
5
+
6
+ __all__ = [
7
+ "BaseWorkspace",
8
+ "LocalWorkspace",
9
+ ]
@@ -0,0 +1,20 @@
1
+ """Abstract base class for workspace containers."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+
6
+ class BaseWorkspace(ABC):
7
+ """Abstract workspace container.
8
+
9
+ A workspace represents the root of the data the agent operates on.
10
+ Implement this class to support custom storage backends.
11
+ """
12
+
13
+ @abstractmethod
14
+ def get_reference(self) -> str:
15
+ """Return the canonical location of this workspace as a string.
16
+
17
+ For local workspaces this is the absolute directory path; other
18
+ backends may return a URI or a cloud bucket path.
19
+ """
20
+ ...
@@ -0,0 +1,25 @@
1
+ """LocalWorkspace — filesystem-backed workspace container."""
2
+
3
+ from pathlib import Path
4
+
5
+ from opendatasci.workspace.base import BaseWorkspace
6
+
7
+
8
+ class LocalWorkspace(BaseWorkspace):
9
+ """Workspace backed by a local directory.
10
+
11
+ Pass a directory to treat the whole directory as the workspace, or pass a
12
+ single file to treat its parent directory as the workspace.
13
+ """
14
+
15
+ def __init__(
16
+ self,
17
+ path: str | Path,
18
+ ) -> None:
19
+ p = Path(path)
20
+ if not p.exists():
21
+ raise FileNotFoundError(f"Path not found: {p}")
22
+ self._directory: Path = (p.parent if p.is_file() else p).resolve()
23
+
24
+ def get_reference(self) -> str:
25
+ return str(self._directory)