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,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,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)
|