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,124 @@
|
|
|
1
|
+
"""AgentLoopCompactor: LLM-based in-turn context compaction for ReAct loops."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
|
|
7
|
+
from langchain_core.messages.utils import count_tokens_approximately
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from opendatasci._utils.langchain_utils import is_ongoing_turn, render_turn
|
|
11
|
+
from opendatasci.prompts.prompt_templates import MIDTURN_COMPACTOR_SYSTEM_PROMPT
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _CompactedAgentLoop(BaseModel):
|
|
17
|
+
content: str
|
|
18
|
+
|
|
19
|
+
def to_message(self) -> SystemMessage:
|
|
20
|
+
"""Return the compacted steps as a SystemMessage for injection into the turn."""
|
|
21
|
+
return SystemMessage(
|
|
22
|
+
content=f"<compacted_agent_loop>\n{self.content}\n</compacted_agent_loop>"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AgentLoopCompactor:
|
|
27
|
+
"""Reduces an in-progress agent turn to fit within the token budget.
|
|
28
|
+
|
|
29
|
+
When a turn's intermediate steps exceed the configured threshold, the
|
|
30
|
+
compactor summarises them before the next LLM call. Only the prompt
|
|
31
|
+
presented to the model is affected; the stored graph state is unchanged.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, llm: Any) -> None:
|
|
35
|
+
self._llm = llm
|
|
36
|
+
self._structured_llm = llm.with_structured_output(_CompactedAgentLoop)
|
|
37
|
+
self._system_prompt = MIDTURN_COMPACTOR_SYSTEM_PROMPT
|
|
38
|
+
|
|
39
|
+
def estimate_tokens(self, messages: list[BaseMessage]) -> int:
|
|
40
|
+
"""Return an approximate token count for *messages*."""
|
|
41
|
+
return count_tokens_approximately(messages)
|
|
42
|
+
|
|
43
|
+
async def compact(self, turn: list[BaseMessage]) -> list[BaseMessage]:
|
|
44
|
+
"""Compact *turn* by summarising intermediate steps and return the reduced message list.
|
|
45
|
+
|
|
46
|
+
The original user message and the most recent agent step are always preserved;
|
|
47
|
+
only the intermediate steps between them are replaced by a summary.
|
|
48
|
+
|
|
49
|
+
Returns *turn* unchanged if it is not an ongoing turn, if there are no
|
|
50
|
+
intermediate steps to compact, or if the LLM call fails.
|
|
51
|
+
"""
|
|
52
|
+
if not is_ongoing_turn(turn):
|
|
53
|
+
return turn
|
|
54
|
+
|
|
55
|
+
# Find the last AIMessage in the turn.
|
|
56
|
+
last_ai_idx = -1
|
|
57
|
+
for i in range(len(turn) - 1, -1, -1):
|
|
58
|
+
if isinstance(turn[i], AIMessage):
|
|
59
|
+
last_ai_idx = i
|
|
60
|
+
break
|
|
61
|
+
|
|
62
|
+
if last_ai_idx == -1:
|
|
63
|
+
return turn
|
|
64
|
+
|
|
65
|
+
intermediate = turn[1:last_ai_idx]
|
|
66
|
+
if not intermediate:
|
|
67
|
+
return turn
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
result: _CompactedAgentLoop = await self._structured_llm.ainvoke(
|
|
71
|
+
[
|
|
72
|
+
SystemMessage(content=self._system_prompt),
|
|
73
|
+
HumanMessage(content=render_turn(intermediate)),
|
|
74
|
+
]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
user_message: BaseMessage = turn[0]
|
|
78
|
+
compaction_message: BaseMessage = result.to_message()
|
|
79
|
+
rest_of_turn: list[BaseMessage] = turn[last_ai_idx:]
|
|
80
|
+
return [user_message, compaction_message, *rest_of_turn]
|
|
81
|
+
except Exception:
|
|
82
|
+
logger.exception("AgentLoopCompactor LLM call failed; return uncompacted turn")
|
|
83
|
+
return turn
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class TurnRewinder:
|
|
87
|
+
"""Removes turns from a conversation history."""
|
|
88
|
+
|
|
89
|
+
def rewind_last_turn(
|
|
90
|
+
self,
|
|
91
|
+
chat_history: list[BaseMessage],
|
|
92
|
+
keep_user_message: bool = False,
|
|
93
|
+
) -> list[BaseMessage]:
|
|
94
|
+
"""Remove the last turn from *chat_history*.
|
|
95
|
+
|
|
96
|
+
The last turn is rewound regardless of whether it has completed — an
|
|
97
|
+
in-progress turn (started but no final agent response yet) is treated
|
|
98
|
+
the same as a completed one.
|
|
99
|
+
|
|
100
|
+
By default the human message that opened the turn is also dropped.
|
|
101
|
+
Pass ``keep_user_message=True`` to retain it and drop only the agent
|
|
102
|
+
response and any intermediate tool messages.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
chat_history: The full conversation message list.
|
|
106
|
+
keep_user_message: When ``True``, the user message that opened
|
|
107
|
+
the last turn is retained. Defaults to ``False``.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
A new list with the last turn removed, or a copy of
|
|
111
|
+
*chat_history* unchanged if no turn start is found.
|
|
112
|
+
"""
|
|
113
|
+
# Locate the HumanMessage that opened the last turn.
|
|
114
|
+
start_idx = -1
|
|
115
|
+
for i in range(len(chat_history) - 1, -1, -1):
|
|
116
|
+
if isinstance(chat_history[i], HumanMessage):
|
|
117
|
+
start_idx = i
|
|
118
|
+
break
|
|
119
|
+
|
|
120
|
+
if start_idx == -1:
|
|
121
|
+
return list(chat_history)
|
|
122
|
+
|
|
123
|
+
cut = start_idx + 1 if keep_user_message else start_idx
|
|
124
|
+
return list(chat_history[:cut])
|
opendatasci/configs.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Configuration for OpenDataSci."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from types import MappingProxyType
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from pydantic import Field, model_validator
|
|
8
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
9
|
+
|
|
10
|
+
from opendatasci.models.providers import Provider
|
|
11
|
+
from opendatasci.skills.local import _BUILTIN_SKILLS_DIRECTORY as _DEFAULT_BUILTIN_SKILLS_DIRECTORY
|
|
12
|
+
|
|
13
|
+
DEFAULT_MODEL: MappingProxyType[Provider, str] = MappingProxyType(
|
|
14
|
+
{
|
|
15
|
+
Provider.ANTHROPIC: "claude-sonnet-4-6",
|
|
16
|
+
Provider.OPENAI: "gpt-5.5",
|
|
17
|
+
Provider.BEDROCK: "us.anthropic.claude-sonnet-4-6",
|
|
18
|
+
Provider.GEMINI: "gemini-2.5-pro",
|
|
19
|
+
Provider.VERTEXAI: "gemini-2.5-pro",
|
|
20
|
+
Provider.AZURE: "gpt-4o",
|
|
21
|
+
Provider.OLLAMA: "llama3.2:3b",
|
|
22
|
+
Provider.OPENAI_COMPATIBLE_SERVER: "meta-llama/Llama-3.2-3B-Instruct",
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
DEFAULT_SECONDARY_MODEL: MappingProxyType[Provider, str] = MappingProxyType(
|
|
27
|
+
{
|
|
28
|
+
Provider.ANTHROPIC: "claude-haiku-4-5",
|
|
29
|
+
Provider.OPENAI: "gpt-5.4-mini",
|
|
30
|
+
Provider.BEDROCK: "us.anthropic.claude-haiku-4-5-20251001-v1:0",
|
|
31
|
+
Provider.GEMINI: "gemini-2.5-flash",
|
|
32
|
+
Provider.VERTEXAI: "gemini-2.5-flash",
|
|
33
|
+
Provider.AZURE: "gpt-4o-mini",
|
|
34
|
+
Provider.OLLAMA: "llama3.2:3b",
|
|
35
|
+
Provider.OPENAI_COMPATIBLE_SERVER: "meta-llama/Llama-3.2-3B-Instruct",
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class OpenDataSciConfig(BaseSettings):
|
|
41
|
+
"""Configuration for OpenDataSci.
|
|
42
|
+
|
|
43
|
+
All fields can be set via environment variables (names shown in parentheses)
|
|
44
|
+
or a ``.env`` file. Pass an instance to :func:`create_agent` or
|
|
45
|
+
:class:`Agent` to apply custom settings.
|
|
46
|
+
|
|
47
|
+
Attributes:
|
|
48
|
+
provider: LLM provider for the primary model. One of
|
|
49
|
+
``"anthropic"``, ``"openai"``, ``"bedrock"``,
|
|
50
|
+
``"gemini"``, ``"vertexai"``, ``"azure"``,
|
|
51
|
+
``"ollama"``, ``"openai_compatible_server"`` (any
|
|
52
|
+
self-hosted OpenAI-compatible server, e.g. vLLM).
|
|
53
|
+
model: Provider-specific model identifier. Falls back to a
|
|
54
|
+
sensible default per provider when not set.
|
|
55
|
+
secondary_provider: LLM provider for the secondary (auxiliary) model.
|
|
56
|
+
Defaults to ``provider`` when not set. Set to a
|
|
57
|
+
different provider to mix backends — e.g. Anthropic
|
|
58
|
+
for the primary model and OpenAI for summarisation.
|
|
59
|
+
secondary_model: Model identifier for lightweight tasks such as memory
|
|
60
|
+
summarisation. Falls back to a sensible default per
|
|
61
|
+
provider when not set.
|
|
62
|
+
anthropic_api_key: API key for Anthropic (``ANTHROPIC_API_KEY``).
|
|
63
|
+
openai_api_key: API key for OpenAI and OpenAI-compatible servers
|
|
64
|
+
(``OPENAI_API_KEY``).
|
|
65
|
+
google_api_key: API key for Google Gemini (``GOOGLE_API_KEY``).
|
|
66
|
+
azure_api_key: API key for Azure OpenAI (``AZURE_OPENAI_API_KEY``).
|
|
67
|
+
Omit when using service-principal auth instead.
|
|
68
|
+
aws_region: AWS region for Bedrock (``REGION``).
|
|
69
|
+
google_cloud_project: GCP project ID for Vertex AI
|
|
70
|
+
(``GOOGLE_CLOUD_PROJECT``).
|
|
71
|
+
google_cloud_location: Vertex AI region / location
|
|
72
|
+
(``GOOGLE_CLOUD_LOCATION``).
|
|
73
|
+
azure_endpoint: Azure OpenAI resource endpoint URL
|
|
74
|
+
(``AZURE_OPENAI_ENDPOINT``). Required when
|
|
75
|
+
``provider`` is ``"azure"``.
|
|
76
|
+
azure_api_version: Azure OpenAI API version
|
|
77
|
+
(``AZURE_OPENAI_API_VERSION``). Defaults to
|
|
78
|
+
``"2025-01-01-preview"``.
|
|
79
|
+
llm_server_base_url: Base URL for self-hosted providers
|
|
80
|
+
(``LLM_SERVER_BASE_URL``). Required for
|
|
81
|
+
``"ollama"`` and ``"openai_compatible_server"``;
|
|
82
|
+
falls back to ``http://localhost:11434`` and
|
|
83
|
+
``http://localhost:8000/v1`` respectively when
|
|
84
|
+
not set.
|
|
85
|
+
temperature: LLM sampling temperature. Ignored for Anthropic and
|
|
86
|
+
Bedrock when extended thinking is active (those
|
|
87
|
+
providers require temperature ``1``).
|
|
88
|
+
thinking_budget: Token budget for extended thinking / reasoning
|
|
89
|
+
(Anthropic and Bedrock only).
|
|
90
|
+
name: Display name of the agent. Defaults to ``"Sai"``.
|
|
91
|
+
mcp_servers: List of MCP server URLs the agent may connect to
|
|
92
|
+
(``MCP_SERVERS``).
|
|
93
|
+
skills_directory: Path to a directory of custom skill files
|
|
94
|
+
(``SKILLS_DIRECTORY``). Loaded in addition to the
|
|
95
|
+
built-in skills; custom skills override built-ins of
|
|
96
|
+
the same name.
|
|
97
|
+
builtin_skills_directory: Path to the built-in skills bundled with
|
|
98
|
+
the package (``BUILTIN_SKILLS_DIRECTORY``). Override
|
|
99
|
+
only if you need to replace the defaults entirely.
|
|
100
|
+
extra_web_domains: Additional hostnames the ``fetch_url`` tool may
|
|
101
|
+
retrieve, on top of the built-in allowlist.
|
|
102
|
+
Example: ``["internal.corp"]``.
|
|
103
|
+
override_web_domains: When set, *replaces* the built-in domain
|
|
104
|
+
allowlist entirely. ``extra_web_domains`` is still
|
|
105
|
+
applied on top. Use ``[]`` to block all domains.
|
|
106
|
+
worker_timeout_seconds: Maximum seconds to wait for all spawned
|
|
107
|
+
workers to finish. ``None`` disables the timeout.
|
|
108
|
+
Defaults to ``300.0`` (5 minutes).
|
|
109
|
+
midturn_compaction_threshold: Token count after which the agent's
|
|
110
|
+
context is compacted mid-turn (during a single turn's
|
|
111
|
+
reasoning/acting loop). Must be strictly positive.
|
|
112
|
+
Defaults to ``80000``.
|
|
113
|
+
local_code_exec_timeout: Maximum seconds allowed for a single
|
|
114
|
+
code-execution run in the local sandbox
|
|
115
|
+
(``CODE_EXEC_TIMEOUT``). Defaults to
|
|
116
|
+
``1800`` (30 minutes).
|
|
117
|
+
Cloud authentication (environment variables read by the underlying SDKs):
|
|
118
|
+
|
|
119
|
+
**AWS Bedrock** — boto3 credential chain (pick one):
|
|
120
|
+
|
|
121
|
+
- Long-lived IAM key: ``AWS_ACCESS_KEY_ID`` + ``AWS_SECRET_ACCESS_KEY``
|
|
122
|
+
- Temporary STS credentials: add ``AWS_SESSION_TOKEN`` to the above.
|
|
123
|
+
- EC2 / ECS / Lambda: credentials are fetched automatically from the
|
|
124
|
+
instance metadata service; no env vars required.
|
|
125
|
+
|
|
126
|
+
**Google Vertex AI** — Application Default Credentials chain (pick one):
|
|
127
|
+
|
|
128
|
+
- Service account JSON key: set ``GOOGLE_APPLICATION_CREDENTIALS`` to
|
|
129
|
+
the path of the key file; also set ``GOOGLE_CLOUD_PROJECT``.
|
|
130
|
+
- User credentials: run ``gcloud auth application-default login``.
|
|
131
|
+
- Cloud Run / GCE / GKE: credentials are fetched automatically;
|
|
132
|
+
``GOOGLE_CLOUD_PROJECT`` is still required.
|
|
133
|
+
|
|
134
|
+
**Azure OpenAI** — API key *or* service principal (not both):
|
|
135
|
+
|
|
136
|
+
- API key: ``AZURE_OPENAI_API_KEY``
|
|
137
|
+
- Service principal (requires ``pip install 'open-data-sci[azure]'``):
|
|
138
|
+
set ``AZURE_TENANT_ID``, ``AZURE_CLIENT_ID``, and
|
|
139
|
+
``AZURE_CLIENT_SECRET``.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
model_config = SettingsConfigDict(
|
|
143
|
+
frozen=True,
|
|
144
|
+
populate_by_name=True,
|
|
145
|
+
env_ignore_empty=True,
|
|
146
|
+
env_file=".env",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# ── Model selection ───────────────────────────────────────────────────────
|
|
150
|
+
provider: Provider = Field(default=Provider.ANTHROPIC, alias="PROVIDER")
|
|
151
|
+
model: str = Field(default=DEFAULT_MODEL[Provider.ANTHROPIC], alias="MODEL")
|
|
152
|
+
secondary_provider: Provider = Field(default=Provider.ANTHROPIC, alias="SECONDARY_PROVIDER")
|
|
153
|
+
secondary_model: str = Field(
|
|
154
|
+
default=DEFAULT_SECONDARY_MODEL[Provider.ANTHROPIC], alias="SECONDARY_MODEL"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# ── Per-provider API keys (loaded from env via alias) ─────────────────────
|
|
158
|
+
anthropic_api_key: str | None = Field(default=None, alias="ANTHROPIC_API_KEY")
|
|
159
|
+
openai_api_key: str | None = Field(default=None, alias="OPENAI_API_KEY")
|
|
160
|
+
google_api_key: str | None = Field(default=None, alias="GOOGLE_API_KEY")
|
|
161
|
+
azure_api_key: str | None = Field(default=None, alias="AZURE_OPENAI_API_KEY")
|
|
162
|
+
|
|
163
|
+
# ── Cloud region / location ───────────────────────────────────────────────
|
|
164
|
+
aws_region: str | None = Field(default=None, alias="REGION")
|
|
165
|
+
google_cloud_project: str | None = Field(default=None, alias="GOOGLE_CLOUD_PROJECT")
|
|
166
|
+
google_cloud_location: str | None = Field(default=None, alias="GOOGLE_CLOUD_LOCATION")
|
|
167
|
+
|
|
168
|
+
# ── Azure-specific ────────────────────────────────────────────────────────
|
|
169
|
+
azure_endpoint: str | None = Field(default=None, alias="AZURE_OPENAI_ENDPOINT")
|
|
170
|
+
azure_api_version: str = Field(default="2025-01-01-preview", alias="AZURE_OPENAI_API_VERSION")
|
|
171
|
+
|
|
172
|
+
# ── Self-hosted endpoint (Ollama / OpenAI-compatible server) ──────────────
|
|
173
|
+
llm_server_base_url: str | None = Field(default=None, alias="LLM_SERVER_BASE_URL")
|
|
174
|
+
|
|
175
|
+
# ── Sampling & reasoning ──────────────────────────────────────────────────
|
|
176
|
+
temperature: float = Field(default=0.0, alias="TEMPERATURE")
|
|
177
|
+
thinking_budget: int = Field(default=8192, alias="THINKING_BUDGET")
|
|
178
|
+
|
|
179
|
+
# ── Agent Customization ───────────────────────────────────────────────────────
|
|
180
|
+
name: str = Field(default="Sai", alias="NAME")
|
|
181
|
+
|
|
182
|
+
# ── MCP ───────────────────────────────────────────────────────────
|
|
183
|
+
mcp_servers: List[str] = Field(default_factory=list, alias="MCP_SERVERS")
|
|
184
|
+
|
|
185
|
+
# ── Web access ───────────────────────────────────────────────────────────────
|
|
186
|
+
extra_web_domains: List[str] = Field(default_factory=list, alias="EXTRA_FETCH_DOMAINS")
|
|
187
|
+
override_web_domains: List[str] | None = None
|
|
188
|
+
|
|
189
|
+
# ── Context management ───────────────────────────────────────────────────────
|
|
190
|
+
midturn_compaction_threshold: int = Field(
|
|
191
|
+
default=64000,
|
|
192
|
+
alias="MIDTURN_COMPACTION_THRESHOLD",
|
|
193
|
+
gt=0,
|
|
194
|
+
description="Token count after which the agent's context is compacted mid-turn. Only applies in execution mode.",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# ── Skills ───────────────────────────────────────────────────────────────────
|
|
198
|
+
skills_directory: Path | None = Field(
|
|
199
|
+
default=None,
|
|
200
|
+
alias="SKILLS_DIRECTORY",
|
|
201
|
+
)
|
|
202
|
+
builtin_skills_directory: Path = Field(
|
|
203
|
+
default=_DEFAULT_BUILTIN_SKILLS_DIRECTORY,
|
|
204
|
+
alias="BUILTIN_SKILLS_DIRECTORY",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# ── Worker configuration ───────────────────────────────────────────────────────
|
|
208
|
+
worker_timeout_seconds: float | None = Field(
|
|
209
|
+
default=300.0,
|
|
210
|
+
alias="WORKER_TIMEOUT_SECONDS",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# ── Sandbox ───────────────────────────────────────────────────────────────────
|
|
214
|
+
local_code_exec_timeout: int = Field(
|
|
215
|
+
default=1800, # 30 minutes
|
|
216
|
+
alias="CODE_EXEC_TIMEOUT",
|
|
217
|
+
gt=0,
|
|
218
|
+
description="Maximum seconds allowed for a single local code-execution run.",
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
@model_validator(mode="after")
|
|
222
|
+
def _validate_providers(self) -> "OpenDataSciConfig":
|
|
223
|
+
if self.provider not in DEFAULT_MODEL:
|
|
224
|
+
supported = ", ".join(f"'{p}'" for p in sorted(DEFAULT_MODEL))
|
|
225
|
+
raise ValueError(
|
|
226
|
+
f"Unknown provider '{self.provider}'. Supported providers: {supported}."
|
|
227
|
+
)
|
|
228
|
+
if self.secondary_provider is not None and self.secondary_provider not in DEFAULT_MODEL:
|
|
229
|
+
supported = ", ".join(f"'{p}'" for p in sorted(DEFAULT_MODEL))
|
|
230
|
+
raise ValueError(
|
|
231
|
+
f"Unknown secondary model provider '{self.secondary_provider}'. "
|
|
232
|
+
f"Supported providers: {supported}."
|
|
233
|
+
)
|
|
234
|
+
return self
|
|
235
|
+
|
|
236
|
+
@classmethod
|
|
237
|
+
def from_yaml(cls, path: str | Path) -> "OpenDataSciConfig":
|
|
238
|
+
"""Load an ``OpenDataSciConfig`` from a YAML file.
|
|
239
|
+
|
|
240
|
+
The file must be a YAML mapping whose keys match ``OpenDataSciConfig``
|
|
241
|
+
field names. Unknown keys raise ``ValueError`` with a descriptive
|
|
242
|
+
message. Environment variables are still applied for any field not
|
|
243
|
+
present in the file.
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
ImportError: If PyYAML is not installed.
|
|
247
|
+
ValueError: If the file does not contain a mapping, or contains
|
|
248
|
+
unknown field names.
|
|
249
|
+
"""
|
|
250
|
+
try:
|
|
251
|
+
import yaml # type: ignore[import-untyped]
|
|
252
|
+
except ImportError as exc:
|
|
253
|
+
raise ImportError(
|
|
254
|
+
"PyYAML is required to load YAML config files. "
|
|
255
|
+
"Install it with: pip install pyyaml"
|
|
256
|
+
) from exc
|
|
257
|
+
|
|
258
|
+
with open(path) as fh:
|
|
259
|
+
data = yaml.safe_load(fh) or {}
|
|
260
|
+
|
|
261
|
+
if not isinstance(data, dict):
|
|
262
|
+
raise ValueError(
|
|
263
|
+
f"YAML config at '{path}' must be a mapping (key: value pairs), "
|
|
264
|
+
f"got {type(data).__name__}"
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
valid_fields = set(cls.model_fields.keys())
|
|
268
|
+
unknown = set(data) - valid_fields
|
|
269
|
+
if unknown:
|
|
270
|
+
raise ValueError(
|
|
271
|
+
f"Unknown fields in YAML config '{path}': {', '.join(sorted(unknown))}. "
|
|
272
|
+
f"Valid fields: {', '.join(sorted(valid_fields))}"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
return cls(**data)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Abstract base classes for the agent's context stores."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from contextlib import AbstractAsyncContextManager
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Self
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseContextStore(ABC):
|
|
10
|
+
"""Context store for dataset notes, profile cards, and session plans.
|
|
11
|
+
|
|
12
|
+
Dataset notes and profile cards persist across agent sessions and are keyed
|
|
13
|
+
by dataset path. Plans are scoped to a session and keyed by ``session_id``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def session(self) -> AbstractAsyncContextManager[Self]:
|
|
18
|
+
"""Return an async context manager that manages this store's lifecycle.
|
|
19
|
+
|
|
20
|
+
Implementations should yield ``self``, performing any required setup on
|
|
21
|
+
entry and teardown (flushing, releasing resources, etc.) on exit.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def root(self) -> Path:
|
|
27
|
+
"""Return the root path of the context store (e.g. the ``.opendatasci`` directory)."""
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
async def read_dataset_info(self, dataset_path: str) -> str:
|
|
31
|
+
"""Return combined dataset info: profile card (if any) + session notes."""
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
async def update_dataset_info(
|
|
35
|
+
self,
|
|
36
|
+
dataset_path: str,
|
|
37
|
+
update: str,
|
|
38
|
+
merge: bool = True,
|
|
39
|
+
) -> str:
|
|
40
|
+
"""Persist dataset notes and return the path to the stored notes file."""
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
async def get_profile_info(self, dataset_path: str) -> tuple[str, str, str | None]:
|
|
44
|
+
"""Return ``(resolved_path_str, hash_hex, existing_profile_or_None)``."""
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def save_dataset_profile(self, hash_hex: str, content: str) -> None:
|
|
48
|
+
"""Persist a completed profile card for *hash_hex*."""
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def current_plan(self, session_id: str) -> str | None:
|
|
52
|
+
"""Return the most recent plan for *session_id*, or ``None``."""
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def save_plan(self, session_id: str, plan: str) -> None:
|
|
56
|
+
"""Persist *plan* for *session_id*."""
|