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
|
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
|
+
]
|