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
File without changes
@@ -0,0 +1,11 @@
1
+ import asyncio
2
+ from concurrent.futures import ThreadPoolExecutor
3
+ from typing import Any
4
+
5
+ _executor = ThreadPoolExecutor(max_workers=16)
6
+
7
+
8
+ async def run_in_executor(func: Any, *args: Any, executor: ThreadPoolExecutor | None = None) -> Any:
9
+ """Run a blocking callable in a thread pool executor."""
10
+ loop = asyncio.get_event_loop()
11
+ return await loop.run_in_executor(executor or _executor, func, *args)
@@ -0,0 +1,135 @@
1
+ """
2
+ File-format detection and extension registry.
3
+
4
+ Centralising format metadata here means ``LocalWorkspace`` does not grow when new
5
+ formats are added — only this module changes. Any other module that needs to
6
+ know which extensions are supported can import from here rather than
7
+ re-declaring its own sets.
8
+ """
9
+
10
+ from pathlib import Path
11
+ from typing import Literal
12
+
13
+ EXCEL_EXTENSIONS: frozenset[str] = frozenset({".xlsx", ".xls", ".xlsm", ".xlsb"})
14
+ CSV_EXTENSIONS: frozenset[str] = frozenset({".csv", ".tsv"})
15
+ JSON_EXTENSIONS: frozenset[str] = frozenset({".json", ".jsonl", ".ndjson"})
16
+ PARQUET_EXTENSIONS: frozenset[str] = frozenset({".parquet", ".pq"})
17
+ ORC_EXTENSIONS: frozenset[str] = frozenset({".orc"})
18
+ FEATHER_EXTENSIONS: frozenset[str] = frozenset({".feather", ".arrow"})
19
+ HDF_EXTENSIONS: frozenset[str] = frozenset({".h5", ".hdf", ".hdf5"})
20
+ PICKLE_EXTENSIONS: frozenset[str] = frozenset({".pkl", ".pickle"})
21
+ XML_EXTENSIONS: frozenset[str] = frozenset({".xml"})
22
+ STATA_EXTENSIONS: frozenset[str] = frozenset({".dta"})
23
+ SAS_EXTENSIONS: frozenset[str] = frozenset({".sas7bdat", ".xpt"})
24
+ SPSS_EXTENSIONS: frozenset[str] = frozenset({".sav", ".zsav"})
25
+ TEXT_EXTENSIONS: frozenset[str] = frozenset(
26
+ {
27
+ ".txt",
28
+ ".md",
29
+ ".log",
30
+ ".yml",
31
+ ".yaml",
32
+ ".toml",
33
+ ".ini",
34
+ ".cfg",
35
+ ".conf",
36
+ ".rst",
37
+ ".text",
38
+ }
39
+ )
40
+ ARCHIVE_EXTENSIONS: frozenset[str] = frozenset({".zip", ".gz", ".bz2", ".xz", ".zst"})
41
+
42
+ # All extensions that can be loaded directly as data (archives are containers,
43
+ # not data files themselves, so they are tracked separately).
44
+ LOADABLE_EXTENSIONS: frozenset[str] = (
45
+ EXCEL_EXTENSIONS
46
+ | CSV_EXTENSIONS
47
+ | JSON_EXTENSIONS
48
+ | PARQUET_EXTENSIONS
49
+ | ORC_EXTENSIONS
50
+ | FEATHER_EXTENSIONS
51
+ | HDF_EXTENSIONS
52
+ | PICKLE_EXTENSIONS
53
+ | XML_EXTENSIONS
54
+ | STATA_EXTENSIONS
55
+ | SAS_EXTENSIONS
56
+ | SPSS_EXTENSIONS
57
+ | TEXT_EXTENSIONS
58
+ )
59
+
60
+ ALL_SUPPORTED_EXTENSIONS: frozenset[str] = LOADABLE_EXTENSIONS | ARCHIVE_EXTENSIONS
61
+
62
+ FileFormat = Literal[
63
+ "excel",
64
+ "csv",
65
+ "json",
66
+ "jsonl",
67
+ "parquet",
68
+ "orc",
69
+ "feather",
70
+ "hdf",
71
+ "pickle",
72
+ "xml",
73
+ "stata",
74
+ "sas",
75
+ "spss",
76
+ "text",
77
+ "zip",
78
+ "unknown",
79
+ ]
80
+
81
+
82
+ def detect_format(path: Path) -> tuple[FileFormat, str | None]:
83
+ """Detect the file format and compression of *path*.
84
+
85
+ Returns:
86
+ A ``(format, compression)`` tuple. ``compression`` is the compression
87
+ scheme without a leading dot (e.g. ``"gz"``) or ``None`` for
88
+ uncompressed files.
89
+
90
+ Examples::
91
+
92
+ detect_format(Path("data.csv")) # ("csv", None)
93
+ detect_format(Path("data.csv.gz")) # ("csv", "gz")
94
+ detect_format(Path("archive.zip")) # ("zip", None)
95
+ """
96
+ suffix = path.suffix.lower()
97
+ compression: str | None = None
98
+ actual_suffix = suffix
99
+
100
+ if suffix in {".gz", ".bz2", ".xz", ".zst"}:
101
+ compression = suffix[1:] # strip leading dot
102
+ actual_suffix = Path(path.stem).suffix.lower()
103
+
104
+ if suffix == ".zip":
105
+ return "zip", None
106
+ if actual_suffix in EXCEL_EXTENSIONS:
107
+ return "excel", compression
108
+ if actual_suffix in CSV_EXTENSIONS:
109
+ return "csv", compression
110
+ if actual_suffix in {".jsonl", ".ndjson"}:
111
+ return "jsonl", compression
112
+ if actual_suffix in JSON_EXTENSIONS:
113
+ return "json", compression
114
+ if actual_suffix in PARQUET_EXTENSIONS:
115
+ return "parquet", compression
116
+ if actual_suffix in ORC_EXTENSIONS:
117
+ return "orc", compression
118
+ if actual_suffix in FEATHER_EXTENSIONS:
119
+ return "feather", compression
120
+ if actual_suffix in HDF_EXTENSIONS:
121
+ return "hdf", compression
122
+ if actual_suffix in PICKLE_EXTENSIONS:
123
+ return "pickle", compression
124
+ if actual_suffix in XML_EXTENSIONS:
125
+ return "xml", compression
126
+ if actual_suffix in STATA_EXTENSIONS:
127
+ return "stata", compression
128
+ if actual_suffix in SAS_EXTENSIONS:
129
+ return "sas", compression
130
+ if actual_suffix in SPSS_EXTENSIONS:
131
+ return "spss", compression
132
+ if actual_suffix in TEXT_EXTENSIONS:
133
+ return "text", compression
134
+
135
+ return "unknown", compression
@@ -0,0 +1,52 @@
1
+ import asyncio
2
+ from pathlib import Path
3
+ from typing import cast
4
+
5
+ import xxhash
6
+
7
+ from opendatasci._utils.async_utils import run_in_executor
8
+
9
+ _HASHING_DATA_CHUNK_SIZE = 32 * 1024 * 1024 # 32MB
10
+
11
+
12
+ def _is_visible(file: Path, root: Path) -> bool:
13
+ """Return ``True`` if *file* is not under a hidden (dot-prefixed) path."""
14
+ return not any(part.startswith(".") for part in file.relative_to(root).parts)
15
+
16
+
17
+ async def hash_path(path: Path) -> str:
18
+ """Return the 128-bit xxh3 hex digest of *path* (file or directory)."""
19
+ if path.is_file():
20
+ return cast(str, await run_in_executor(hash_file, path))
21
+ return await hash_dir(path)
22
+
23
+
24
+ def hash_file(path: Path, chunk_size: int | None = None) -> str:
25
+ """Return the 128-bit xxh3 hex digest of a file's contents."""
26
+ chunk_size = chunk_size or _HASHING_DATA_CHUNK_SIZE
27
+ h = xxhash.xxh3_128()
28
+ with path.open("rb") as f:
29
+ for chunk in iter(lambda: f.read(chunk_size), b""):
30
+ h.update(chunk)
31
+ return h.hexdigest()
32
+
33
+
34
+ async def hash_dir(path: Path) -> str:
35
+ """Return a 128-bit hash of a directory tree, excluding hidden/system files."""
36
+ files: list[Path] = sorted(
37
+ (f for f in path.rglob("*") if f.is_file() and _is_visible(f, path)),
38
+ key=lambda p: str(p),
39
+ )
40
+
41
+ if not files:
42
+ return xxhash.xxh3_128(b"").hexdigest()
43
+
44
+ coros = [run_in_executor(hash_file, f) for f in files]
45
+ hashes: list[str] = await asyncio.gather(*coros)
46
+
47
+ h_prev: bytes = b""
48
+ for h in hashes:
49
+ file_hash = xxhash.xxh3_128(bytes.fromhex(h)).digest()
50
+ h_prev = xxhash.xxh3_128(h_prev + file_hash).digest()
51
+
52
+ return h_prev.hex()
@@ -0,0 +1,155 @@
1
+ """LangChain / LangGraph message and state utilities."""
2
+
3
+ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage, ToolMessage
4
+ from langgraph.types import StateSnapshot
5
+
6
+
7
+ def get_message_text_content(msg: BaseMessage) -> str:
8
+ """Extract plain text from a message, skipping non-text blocks (e.g. thinking)."""
9
+ content = msg.content
10
+ if isinstance(content, str):
11
+ return content
12
+ if not isinstance(content, list):
13
+ return str(content)
14
+ parts: list[str] = []
15
+ for block in content:
16
+ if isinstance(block, dict):
17
+ if block.get("type") == "text":
18
+ parts.append(block.get("text", ""))
19
+ elif isinstance(block, str):
20
+ parts.append(block)
21
+ return "\n".join(parts)
22
+
23
+
24
+ def render_turn(messages: list[BaseMessage]) -> str:
25
+ """Render a sequence of messages (a full turn or an ongoing slice) as a readable string.
26
+
27
+ Each message type is formatted as follows:
28
+
29
+ - ``HumanMessage`` → ``User: <content>``
30
+ - ``AIMessage`` with tool calls → ``[TOOL CALL: <name>]\\n<args>`` (one entry per call)
31
+ - ``AIMessage`` without tool calls → ``Agent: <text>``
32
+ - ``ToolMessage`` → ``[TOOL OUTPUT]\\n<content>``
33
+
34
+ Other message types are silently skipped.
35
+ """
36
+ parts: list[str] = []
37
+ for msg in messages:
38
+ if isinstance(msg, HumanMessage):
39
+ content = msg.content if isinstance(msg.content, str) else str(msg.content)
40
+ content = content.strip()
41
+ if content:
42
+ parts.append(f"User: {content}")
43
+ elif isinstance(msg, AIMessage) and msg.tool_calls:
44
+ for tc in msg.tool_calls:
45
+ args_str = str(tc.get("args", {}))
46
+ parts.append(f"[TOOL CALL: {tc['name']}]\n{args_str}")
47
+ elif isinstance(msg, AIMessage):
48
+ text = get_message_text_content(msg).strip()
49
+ if text:
50
+ parts.append(f"Agent: {text}")
51
+ elif isinstance(msg, ToolMessage):
52
+ content = msg.content if isinstance(msg.content, str) else str(msg.content)
53
+ parts.append(f"[TOOL OUTPUT]\n{content}")
54
+ return "\n\n".join(parts) if parts else "(no messages)"
55
+
56
+
57
+ def render_turns(turns: list[list[BaseMessage]]) -> str:
58
+ """Render a list of turns as a single readable string.
59
+
60
+ Each turn is rendered via :func:`render_turn` and separated by a blank line.
61
+ Returns ``"(no conversation to render)"`` when *turns* is empty.
62
+ """
63
+ rendered = [render_turn(t) for t in turns if t]
64
+ return "\n\n".join(rendered) if rendered else "(no conversation to render)"
65
+
66
+
67
+ def prepend_messages(
68
+ history: list[BaseMessage],
69
+ messages: list[BaseMessage],
70
+ ) -> list[BaseMessage]:
71
+ """Prepend *messages* to *history*, dropping any existing SystemMessages from *history*."""
72
+ non_system = [m for m in history if not isinstance(m, SystemMessage)]
73
+ return messages + non_system
74
+
75
+
76
+ def is_interrupt_state_snapshot(state: StateSnapshot) -> bool:
77
+ """Return True if *state* contains at least one pending LangGraph interrupt."""
78
+ return any(intr for task in state.tasks for intr in task.interrupts)
79
+
80
+
81
+ def is_final_ai_message(msg: BaseMessage) -> bool:
82
+ """Return True if *msg* is an AIMessage with no pending tool calls."""
83
+ return isinstance(msg, AIMessage) and not bool(getattr(msg, "tool_calls", None))
84
+
85
+
86
+ def get_ongoing_turn_messages(messages: list[BaseMessage]) -> list[BaseMessage]:
87
+ """Return the messages of the current (still-in-progress) conversation turn.
88
+
89
+ Uses the same turn-boundary rules as ``get_last_turn_messages``: the turn
90
+ begins at the most recent turn-opening ``HumanMessage`` (one whose
91
+ ``additional_kwargs`` does **not** set ``is_input_on_interrupt`` to ``True``).
92
+
93
+ Raises:
94
+ ValueError: if the turn is already complete (not ongoing).
95
+ """
96
+ for i in range(len(messages) - 1, -1, -1):
97
+ msg = messages[i]
98
+ if isinstance(msg, HumanMessage) and not msg.additional_kwargs.get(
99
+ "is_input_on_interrupt", False
100
+ ):
101
+ turn = messages[i:]
102
+ if not is_ongoing_turn(turn):
103
+ raise ValueError("Current turn is already complete")
104
+ return turn
105
+ return []
106
+
107
+
108
+ def get_last_turn_messages(messages: list[BaseMessage]) -> list[BaseMessage]:
109
+ """Return the messages of the most recent conversation turn.
110
+
111
+ A turn begins at the most recent *turn-opening* ``HumanMessage`` — one whose
112
+ ``additional_kwargs`` does **not** flag it as an interrupt reply
113
+ (``is_input_on_interrupt`` is ``False`` or absent) — and extends to the end of
114
+ *messages*. HumanMessages flagged as interrupt replies are skipped, so a turn
115
+ that paused to ask the user a question is still treated as a single turn.
116
+
117
+ Returns ``[]`` when no turn-opening HumanMessage exists.
118
+ """
119
+ for i in range(len(messages) - 1, -1, -1):
120
+ msg = messages[i]
121
+ if isinstance(msg, HumanMessage) and not msg.additional_kwargs.get(
122
+ "is_input_on_interrupt", False
123
+ ):
124
+ return messages[i:]
125
+ return []
126
+
127
+
128
+ def get_final_ai_message(chat_history: list[BaseMessage]) -> AIMessage:
129
+ """Return the last AIMessage in *chat_history*, or raise ValueError if none exists."""
130
+ for msg in reversed(chat_history):
131
+ if isinstance(msg, AIMessage):
132
+ return msg
133
+ raise ValueError("No AIMessage found in chat history")
134
+
135
+
136
+ def is_ongoing_turn(turn: list[BaseMessage]) -> bool:
137
+ """Return True if *turn* is an active, in-progress ReAct turn.
138
+
139
+ A valid ongoing turn starts with a HumanMessage and ends with either an
140
+ AIMessage carrying pending tool calls, a ToolMessage (tool results not yet
141
+ processed by the agent), or an interrupt-reply HumanMessage (the agent
142
+ paused to ask the user a question and has not yet resumed).
143
+ """
144
+ if not turn:
145
+ return False
146
+ if not isinstance(turn[0], HumanMessage):
147
+ return False
148
+ last = turn[-1]
149
+ if isinstance(last, ToolMessage):
150
+ return True
151
+ if isinstance(last, HumanMessage) and last.additional_kwargs.get(
152
+ "is_input_on_interrupt", False
153
+ ):
154
+ return True
155
+ return isinstance(last, AIMessage) and bool(last.tool_calls)
@@ -0,0 +1,23 @@
1
+ _CONNECTION_EXC_NAMES = frozenset(
2
+ ["ConnectError", "APIConnectionError", "ConnectionError", "NetworkError"]
3
+ )
4
+ _CONNECTION_KEYWORDS = frozenset(
5
+ ["connection error", "connection refused", "name or service not known"]
6
+ )
7
+
8
+
9
+ def format_stream_error(exc: Exception) -> str:
10
+ """Return a user-friendly error message for a streaming exception."""
11
+ exc_type = type(exc).__name__
12
+ msg = str(exc)
13
+ msg_lower = msg.lower()
14
+ if exc_type in _CONNECTION_EXC_NAMES or any(kw in msg_lower for kw in _CONNECTION_KEYWORDS):
15
+ return (
16
+ "Connection error — could not reach the API. "
17
+ "Check your internet connection and verify your API key is valid."
18
+ )
19
+ if exc_type in ("AuthenticationError", "AuthError") or any(
20
+ kw in msg_lower for kw in ("authentication", "api_key", "invalid x-api-key", "unauthorized")
21
+ ):
22
+ return f"Authentication error — your API key may be invalid or expired. ({msg})"
23
+ return msg
@@ -0,0 +1,12 @@
1
+ """Agent layer — LLM provider factory, memory, tools, skills, and the Agent class."""
2
+
3
+ from opendatasci.agents.agents import Agent
4
+ from opendatasci.agents.agents_factory import create_agent
5
+ from opendatasci.agents.chat_memory import ChatHistoryBuilder, PreparedHistory
6
+
7
+ __all__ = [
8
+ "Agent",
9
+ "create_agent",
10
+ "ChatHistoryBuilder",
11
+ "PreparedHistory",
12
+ ]