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,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,7 @@
1
+ from opendatasci.context.base import BaseContextStore
2
+ from opendatasci.context.local import LocalContextStore
3
+
4
+ __all__ = [
5
+ "BaseContextStore",
6
+ "LocalContextStore",
7
+ ]
@@ -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*."""