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,159 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import TYPE_CHECKING, ClassVar
5
+
6
+ if TYPE_CHECKING:
7
+ from langchain_core.messages import BaseMessage
8
+
9
+
10
+ @dataclass
11
+ class BaseAgentStreamEvent:
12
+ """Base class for all streaming agent events."""
13
+
14
+ type: ClassVar[str] = ""
15
+
16
+
17
+ @dataclass
18
+ class ReasoningEvent(BaseAgentStreamEvent):
19
+ """Extended-thinking / reasoning token(s)."""
20
+
21
+ type: ClassVar[str] = "reasoning"
22
+ content: str = ""
23
+
24
+
25
+ @dataclass
26
+ class TokenEvent(BaseAgentStreamEvent):
27
+ """Regular response text token."""
28
+
29
+ type: ClassVar[str] = "token"
30
+ content: str = ""
31
+
32
+
33
+ @dataclass
34
+ class ToolCallEvent(BaseAgentStreamEvent):
35
+ """The agent is invoking a tool.
36
+
37
+ ``worker_summaries`` is populated only for ``spawn_workers`` tool calls;
38
+ ``summary`` carries the agent-provided summary argument for all other calls.
39
+ """
40
+
41
+ type: ClassVar[str] = "tool_call"
42
+ content: str = ""
43
+ tool: str = ""
44
+ tool_call_id: str | None = None
45
+ summary: str = ""
46
+ worker_summaries: list[str] = field(default_factory=list)
47
+
48
+
49
+ @dataclass
50
+ class ToolCommunicationEvent(BaseAgentStreamEvent):
51
+ """A progress message emitted by a tool before it returns."""
52
+
53
+ type: ClassVar[str] = "tool_communication"
54
+ content: str = ""
55
+ tool_call_id: str = ""
56
+ tool_name: str = ""
57
+
58
+
59
+ @dataclass
60
+ class ToolResultEvent(BaseAgentStreamEvent):
61
+ """A tool returned a result."""
62
+
63
+ type: ClassVar[str] = "tool_result"
64
+ content: str = ""
65
+ tool_call_id: str | None = None
66
+ is_error: bool = False
67
+
68
+
69
+ @dataclass
70
+ class MessageEvent(BaseAgentStreamEvent):
71
+ """A completed ``BaseMessage`` for callers that own conversation-history accumulation."""
72
+
73
+ type: ClassVar[str] = "message"
74
+ message: BaseMessage | None = None
75
+
76
+
77
+ @dataclass
78
+ class WorkerDoneEvent(BaseAgentStreamEvent):
79
+ """A single concurrent worker finished."""
80
+
81
+ type: ClassVar[str] = "worker_done"
82
+ worker_idx: int | None = None
83
+ success: bool = True
84
+
85
+
86
+ @dataclass
87
+ class SubagentEvent(BaseAgentStreamEvent):
88
+ """Lifecycle event from inside a running worker.
89
+
90
+ ``event_type`` is one of ``"worker_tool_call"`` or ``"worker_tool_result"``.
91
+ ``content`` carries the tool name for ``worker_tool_call`` events.
92
+ """
93
+
94
+ type: ClassVar[str] = "subagent_event"
95
+ content: str = ""
96
+ worker_idx: int | None = None
97
+ event_type: str = ""
98
+ success: bool = True
99
+ summary: str = ""
100
+
101
+
102
+ @dataclass
103
+ class InputRequiredEvent(BaseAgentStreamEvent):
104
+ """The agent is paused at an interrupt and needs input from the user.
105
+
106
+ ``content`` is the question. Call ``astream`` again with the user's
107
+ answer to resume.
108
+ """
109
+
110
+ type: ClassVar[str] = "input_required"
111
+ content: str = ""
112
+ choices: list[str] = field(default_factory=list)
113
+
114
+
115
+ @dataclass
116
+ class UsageEvent(BaseAgentStreamEvent):
117
+ """Per-call token usage.
118
+
119
+ All fields are ``None`` when not reported by the underlying provider for
120
+ this event (e.g. incremental estimates omit cache fields).
121
+ """
122
+
123
+ type: ClassVar[str] = "usage"
124
+ input_tokens: int | None = None
125
+ output_tokens: int | None = None
126
+ cache_read_tokens: int | None = None
127
+ cache_creation_tokens: int | None = None
128
+
129
+
130
+ @dataclass
131
+ class ResponseEvent(BaseAgentStreamEvent):
132
+ """Final assembled response for this turn (end-of-turn marker)."""
133
+
134
+ type: ClassVar[str] = "response"
135
+ content: str = ""
136
+
137
+
138
+ @dataclass
139
+ class ErrorEvent(BaseAgentStreamEvent):
140
+ """An unrecoverable error occurred."""
141
+
142
+ type: ClassVar[str] = "error"
143
+ content: str = ""
144
+
145
+
146
+ AgentStreamEvent = (
147
+ ReasoningEvent
148
+ | TokenEvent
149
+ | ToolCallEvent
150
+ | ToolCommunicationEvent
151
+ | ToolResultEvent
152
+ | MessageEvent
153
+ | WorkerDoneEvent
154
+ | SubagentEvent
155
+ | InputRequiredEvent
156
+ | UsageEvent
157
+ | ResponseEvent
158
+ | ErrorEvent
159
+ )
@@ -0,0 +1,387 @@
1
+ import logging
2
+ import re
3
+ from typing import Any
4
+
5
+ from langchain_core.messages import AIMessage, ToolMessage
6
+
7
+ from opendatasci._utils.langchain_utils import get_message_text_content
8
+ from opendatasci.streaming.events import (
9
+ AgentStreamEvent,
10
+ MessageEvent,
11
+ ReasoningEvent,
12
+ SubagentEvent,
13
+ TokenEvent,
14
+ ToolCallEvent,
15
+ ToolCommunicationEvent,
16
+ ToolResultEvent,
17
+ UsageEvent,
18
+ WorkerDoneEvent,
19
+ )
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ SUBAGENT_TAG: str = "opendatasci:subagent"
25
+
26
+
27
+ # Regex to extract the `communication` value from a partial tool-call JSON string.
28
+ _COMM_RE = re.compile(r'"communication"\s*:\s*"((?:[^"\\]|\\.)*)"')
29
+
30
+
31
+ class AgentTurnStreamProcessor:
32
+ """Converts raw LangGraph stream events into AgentStreamEvent objects.
33
+
34
+ Maintains per-stream state (current text buffer, pending tool calls, etc.).
35
+ Call ``process_event`` for each raw event; it returns zero or more
36
+ ``AgentStreamEvent`` objects to yield. ``MessageEvent`` events carry
37
+ completed ``BaseMessage`` objects and are intended for callers that own
38
+ conversation-history accumulation.
39
+ """
40
+
41
+ def __init__(self) -> None:
42
+ # Per-turn translation state
43
+ self._has_streamed_tokens: bool = False
44
+ self._last_communication: dict[str, str] = {}
45
+ self._pending_tool_calls: list[dict[str, Any]] = []
46
+ # Per-model-call state for incremental usage estimates
47
+ self._stream_input_tokens: int | None = None
48
+ self._stream_output_chars: int = 0
49
+
50
+ def process_event(self, event: dict[str, Any]) -> list[AgentStreamEvent]:
51
+ """Process one raw graph event and return any StreamEvents to emit."""
52
+ # Drop events that bubbled up from a sub-agent (ConcurrentWorkerAgent) so the
53
+ # TUI sees only the main agent's narration, tool calls, and the
54
+ # high-level worker progress signals. Worker activity is surfaced via
55
+ # ``SubagentEvent``/``WorkerDoneEvent`` events instead.
56
+ if SUBAGENT_TAG in (event.get("tags") or ()):
57
+ return []
58
+ kind = event.get("event", "")
59
+ if kind == "on_chat_model_stream":
60
+ return self._handle_stream(event)
61
+ if kind == "on_chain_end" and event.get("name") == "agent":
62
+ return self._handle_chain_end(event)
63
+ if kind == "on_tool_end":
64
+ return self._handle_tool_end(event)
65
+ if kind == "on_chat_model_end":
66
+ return self._handle_model_end(event)
67
+ if kind == "on_custom_event" and event.get("name") == "worker_event":
68
+ return self._handle_worker_event(event)
69
+ return []
70
+
71
+ def _handle_stream(self, event: dict[str, Any]) -> list[AgentStreamEvent]:
72
+ out: list[AgentStreamEvent] = []
73
+ chunk = event.get("data", {}).get("chunk")
74
+ if not chunk:
75
+ return out
76
+ out.extend(self._extract_content_events(chunk.content))
77
+ self._accumulate_tool_call_chunks(getattr(chunk, "tool_call_chunks", []), out)
78
+ self._update_stream_usage(chunk, out)
79
+ return out
80
+
81
+ def _extract_content_events(self, content: Any) -> list[AgentStreamEvent]:
82
+ """Convert chunk content (str or list of blocks) to TokenEvent/ReasoningEvent objects."""
83
+ out: list[AgentStreamEvent] = []
84
+ if isinstance(content, list):
85
+ for block in content:
86
+ if isinstance(block, dict):
87
+ btype = block.get("type", "")
88
+ if btype == "thinking":
89
+ token = block.get("thinking", "")
90
+ if token:
91
+ out.append(ReasoningEvent(content=token))
92
+ elif btype == "reasoning_content":
93
+ # Bedrock ConverseStream format
94
+ token = block.get("reasoning_content", {}).get("text", "")
95
+ if token:
96
+ out.append(ReasoningEvent(content=token))
97
+ elif btype == "text":
98
+ token = block.get("text", "")
99
+ if token:
100
+ self._has_streamed_tokens = True
101
+ out.append(TokenEvent(content=token))
102
+ elif isinstance(block, str) and block:
103
+ self._has_streamed_tokens = True
104
+ out.append(TokenEvent(content=block))
105
+ else:
106
+ logger.error(
107
+ "Unhandled stream block type: %s, value: %r",
108
+ type(block).__name__,
109
+ block,
110
+ )
111
+ elif isinstance(content, str) and content:
112
+ self._has_streamed_tokens = True
113
+ out.append(TokenEvent(content=content))
114
+ return out
115
+
116
+ def _resolve_tool_call_target(self, chunk_index: int | None) -> dict[str, Any] | None:
117
+ """Return the pending tool call to update for the given chunk index."""
118
+ if not self._pending_tool_calls:
119
+ return None
120
+ if chunk_index is not None:
121
+ return next(
122
+ (tc for tc in self._pending_tool_calls if tc.get("index") == chunk_index),
123
+ self._pending_tool_calls[-1],
124
+ )
125
+ return self._pending_tool_calls[-1]
126
+
127
+ def _accumulate_tool_call_chunks(
128
+ self, tc_chunks: list[Any], out: list[AgentStreamEvent]
129
+ ) -> None:
130
+ """Accumulate tool-call arg chunks and emit ToolCommunicationEvent objects."""
131
+ for tc_chunk in tc_chunks:
132
+ chunk_index = tc_chunk.get("index")
133
+ if tc_chunk.get("name"):
134
+ self._pending_tool_calls.append(
135
+ {
136
+ "name": tc_chunk.get("name"),
137
+ "args": tc_chunk.get("args") or "",
138
+ "id": tc_chunk.get("id"),
139
+ "index": chunk_index,
140
+ }
141
+ )
142
+ elif tc_chunk.get("args"):
143
+ # Route args to the tool call identified by index (parallel tool calls).
144
+ # Fall back to the last tool call when no index is present.
145
+ target = self._resolve_tool_call_target(chunk_index)
146
+ if target is not None:
147
+ target["args"] += tc_chunk.get("args") or ""
148
+
149
+ # Inspect the same tool call that was just updated so that each
150
+ # parallel tool call's communication is emitted independently.
151
+ current_tc = self._resolve_tool_call_target(chunk_index)
152
+ if current_tc is None:
153
+ continue
154
+ args = current_tc["args"]
155
+ tc_id = current_tc.get("id") or ""
156
+ m = _COMM_RE.search(args) if '"communication"' in args else None
157
+ if m:
158
+ comm = m.group(1)
159
+ if comm != self._last_communication.get(tc_id, ""):
160
+ self._last_communication[tc_id] = comm
161
+ out.append(
162
+ ToolCommunicationEvent(
163
+ content=comm,
164
+ tool_call_id=tc_id,
165
+ tool_name=current_tc.get("name", ""),
166
+ )
167
+ )
168
+
169
+ def _update_stream_usage(self, chunk: Any, out: list[AgentStreamEvent]) -> None:
170
+ """Capture input tokens once and emit an incremental usage estimate per text chunk."""
171
+ # Anthropic includes input_tokens in the initial message_start chunk.
172
+ usage_meta = getattr(chunk, "usage_metadata", None)
173
+ if isinstance(usage_meta, dict) and self._stream_input_tokens is None:
174
+ in_tok = usage_meta.get("input_tokens")
175
+ if in_tok:
176
+ self._stream_input_tokens = int(in_tok)
177
+
178
+ chars_this_call = sum(len(ev.content) for ev in out if isinstance(ev, TokenEvent))
179
+ if chars_this_call > 0 and self._stream_input_tokens is not None:
180
+ self._stream_output_chars += chars_this_call
181
+ out.append(
182
+ UsageEvent(
183
+ input_tokens=self._stream_input_tokens,
184
+ output_tokens=max(1, self._stream_output_chars // 4),
185
+ )
186
+ )
187
+
188
+ def _handle_chain_end(self, event: dict[str, Any]) -> list[AgentStreamEvent]:
189
+ out: list[AgentStreamEvent] = []
190
+ output_messages = event.get("data", {}).get("output", {}).get("messages", [])
191
+
192
+ for msg in output_messages:
193
+ if not isinstance(msg, AIMessage):
194
+ continue
195
+ out.append(MessageEvent(message=msg))
196
+
197
+ if getattr(msg, "tool_calls", None):
198
+ for tc in msg.tool_calls:
199
+ out.extend(self._handle_tool_call(tc))
200
+ else:
201
+ if not self._has_streamed_tokens:
202
+ # Streaming didn't capture any tokens for this step.
203
+ # This can happen when ChatAnthropic._agenerate falls
204
+ # through to the non-streaming API call (streaming=False
205
+ # and no _StreamingCallbackHandler in the run manager).
206
+ # The complete text is always on the AIMessage, so extract
207
+ # it as a fallback so the response is never silently dropped.
208
+ logger.warning(
209
+ "_handle_chain_end: no tokens were streamed but AIMessage "
210
+ "has content — streaming tokens were not captured for this "
211
+ "step. Falling back to msg.content."
212
+ )
213
+ fallback_text = get_message_text_content(msg).strip()
214
+ if fallback_text:
215
+ out.append(TokenEvent(content=fallback_text))
216
+
217
+ # Reset per-message accumulators.
218
+ self._has_streamed_tokens = False
219
+ self._last_communication = {}
220
+ self._pending_tool_calls = []
221
+ self._stream_input_tokens = None
222
+ self._stream_output_chars = 0
223
+ return out
224
+
225
+ def _handle_tool_call(self, tc: Any) -> list[AgentStreamEvent]:
226
+ from opendatasci.tools import ToolName # local import breaks circular dependency
227
+
228
+ name = tc["name"]
229
+ args = tc["args"] if isinstance(tc["args"], dict) else {}
230
+ events: list[AgentStreamEvent] = []
231
+
232
+ # Guarantee tool_communication is emitted before tool_call using the
233
+ # complete, finalised args. The streaming path (_handle_stream) emits
234
+ # it incrementally as chunks arrive, but it may miss the value when the
235
+ # model does not stream tool-call args or when the closing quote of the
236
+ # communication string only arrives in the last chunk. Using
237
+ # _last_communication as a guard avoids emitting a duplicate when the
238
+ # streaming path already captured the value.
239
+ tc_id = tc.get("id") or ""
240
+ comm = args.get("communication", "")
241
+ if comm and comm != self._last_communication.get(tc_id, ""):
242
+ self._last_communication[tc_id] = comm
243
+ events.append(
244
+ ToolCommunicationEvent(
245
+ content=comm,
246
+ tool_call_id=tc_id,
247
+ tool_name=name,
248
+ )
249
+ )
250
+
251
+ # spawn_workers carries worker_summaries instead of a plain summary.
252
+ if name == ToolName.SPAWN_WORKERS:
253
+ tasks_arg = args.get("subtasks", [])
254
+ worker_summaries = [
255
+ t.get("summary", f"ConcurrentWorkerAgent {i + 1}")
256
+ if isinstance(t, dict)
257
+ else f"ConcurrentWorkerAgent {i + 1}"
258
+ for i, t in enumerate(tasks_arg)
259
+ ]
260
+ events.append(
261
+ ToolCallEvent(
262
+ content=str(tc["args"]),
263
+ # ``ToolName`` is a (str, Enum), and ``str(member)`` returns
264
+ # ``"ToolName.SPAWN_WORKERS"`` rather than ``"spawn_workers"``.
265
+ # Storing ``name`` here keeps this branch consistent with the
266
+ # generic branch below — the presenter does
267
+ # ``str(event.tool)`` before comparing, so storing the enum
268
+ # here breaks its spawn_workers detection.
269
+ tool=name,
270
+ tool_call_id=tc.get("id"),
271
+ worker_summaries=worker_summaries,
272
+ )
273
+ )
274
+ return events
275
+
276
+ events.append(
277
+ ToolCallEvent(
278
+ content=str(tc["args"]),
279
+ tool=name,
280
+ tool_call_id=tc.get("id"),
281
+ summary=args.get("summary", ""),
282
+ )
283
+ )
284
+ return events
285
+
286
+ def _handle_worker_event(self, event: dict[str, Any]) -> list[AgentStreamEvent]:
287
+ data = event.get("data", {})
288
+ event_type = data.get("event_type", "")
289
+ worker_idx = data.get("worker_idx")
290
+ if event_type == "worker_done":
291
+ return [
292
+ WorkerDoneEvent(
293
+ worker_idx=worker_idx,
294
+ success=data.get("success", True),
295
+ )
296
+ ]
297
+ return [
298
+ SubagentEvent(
299
+ content=data.get("content", ""),
300
+ worker_idx=worker_idx,
301
+ event_type=event_type,
302
+ success=data.get("success", True),
303
+ summary=data.get("summary", ""),
304
+ )
305
+ ]
306
+
307
+ @staticmethod
308
+ def _unwrap_command_output(output: Any) -> Any:
309
+ """Return the ToolMessage carried by a langgraph ``Command``.
310
+
311
+ State-mutating tools (``load_skill``, ``enter_plan_mode``,
312
+ ``exit_plan_mode``) return a ``Command`` whose ToolMessage lives in
313
+ ``update["messages"]`` instead of being returned directly. A
314
+ ``Command`` exposes neither ``content`` nor ``tool_call_id``, so
315
+ without this unwrap the resulting ``ToolResultEvent`` carries
316
+ ``tool_call_id=None`` and the TUI can never correlate it with the
317
+ running ephemeral block — leaving the spinner spinning forever.
318
+ """
319
+ update = getattr(output, "update", None)
320
+ if isinstance(update, dict):
321
+ messages = update.get("messages")
322
+ if isinstance(messages, list):
323
+ for msg in reversed(messages):
324
+ if isinstance(msg, ToolMessage):
325
+ return msg
326
+ return output
327
+
328
+ def _handle_tool_end(self, event: dict[str, Any]) -> list[AgentStreamEvent]:
329
+ output = event.get("data", {}).get("output")
330
+ if not output:
331
+ return []
332
+ output = self._unwrap_command_output(output)
333
+ tool_content = output.content if hasattr(output, "content") else str(output)
334
+ is_error = (
335
+ isinstance(output, ToolMessage) and getattr(output, "status", "success") == "error"
336
+ )
337
+ # Prefer tool_call_id from the output (ToolMessage); fall back to event metadata
338
+ # for tools (e.g. StructuredTool via MCP) where on_tool_end fires with the raw
339
+ # return value before LangGraph wraps it in a ToolMessage.
340
+ tool_call_id = getattr(output, "tool_call_id", None) or event.get("metadata", {}).get(
341
+ "tool_call_id"
342
+ )
343
+ out: list[AgentStreamEvent] = []
344
+ if isinstance(output, ToolMessage):
345
+ out.append(MessageEvent(message=output))
346
+ out.append(
347
+ ToolResultEvent(
348
+ content=tool_content,
349
+ tool_call_id=tool_call_id,
350
+ is_error=is_error,
351
+ )
352
+ )
353
+ return out
354
+
355
+ def _handle_model_end(self, event: dict[str, Any]) -> list[AgentStreamEvent]:
356
+ output_msg = event.get("data", {}).get("output")
357
+ if output_msg:
358
+ usage = getattr(output_msg, "usage_metadata", None)
359
+ if usage:
360
+ input_tokens = usage.get("input_tokens", 0)
361
+ output_tokens = usage.get("output_tokens", 0)
362
+ cache_read_tokens = 0
363
+ cache_creation_tokens = 0
364
+ # Anthropic direct: cache tokens nested under input_token_details
365
+ details = usage.get("input_token_details", {})
366
+ if isinstance(details, dict):
367
+ cache_read_tokens += details.get("cache_read", 0)
368
+ cache_creation_tokens += details.get("cache_creation", 0)
369
+ # Bedrock (langchain_aws): cache tokens at top level after
370
+ # cacheReadInputTokens → cache_read_input_tokens conversion.
371
+ # Bedrock reports input_tokens as the non-cached portion only, so
372
+ # add cache tokens back to input_tokens to match Anthropic's convention
373
+ # (where input_tokens already includes the cached subset).
374
+ bedrock_cache_read = usage.get("cache_read_input_tokens", 0)
375
+ bedrock_cache_write = usage.get("cache_write_input_tokens", 0)
376
+ cache_read_tokens += bedrock_cache_read
377
+ cache_creation_tokens += bedrock_cache_write
378
+ input_tokens += bedrock_cache_read + bedrock_cache_write
379
+ return [
380
+ UsageEvent(
381
+ input_tokens=input_tokens,
382
+ output_tokens=output_tokens,
383
+ cache_read_tokens=cache_read_tokens,
384
+ cache_creation_tokens=cache_creation_tokens,
385
+ )
386
+ ]
387
+ return []
@@ -0,0 +1,58 @@
1
+ from opendatasci.tools.coding import (
2
+ create_cli_tools,
3
+ create_code_verification_tools,
4
+ create_coding_tools,
5
+ )
6
+ from opendatasci.tools.critic import create_critic_tools
7
+ from opendatasci.tools.dataset_info import (
8
+ build_profile_code,
9
+ create_data_context_tools,
10
+ create_profile_dataset_tools,
11
+ create_read_dataset_info_tools,
12
+ )
13
+ from opendatasci.tools.factory import (
14
+ ToolName,
15
+ create_agent_tools,
16
+ create_worker_agent_tools,
17
+ )
18
+ from opendatasci.tools.mcp import create_mcp_tools, load_mcp_servers
19
+ from opendatasci.tools.planning import create_planning_tools
20
+ from opendatasci.tools.skills import create_skill_tools
21
+ from opendatasci.tools.user_interaction import create_user_interaction_tools
22
+ from opendatasci.tools.web import create_web_tools
23
+ from opendatasci.tools.workers import WorkerTask, create_worker_tools
24
+ from opendatasci.tools.workspace import create_workspace_tools
25
+
26
+ __all__ = [
27
+ # coding
28
+ "create_cli_tools",
29
+ "create_code_verification_tools",
30
+ "create_coding_tools",
31
+ # critic
32
+ "create_critic_tools",
33
+ # dataset_info
34
+ "build_profile_code",
35
+ "create_data_context_tools",
36
+ "create_read_dataset_info_tools",
37
+ "create_profile_dataset_tools",
38
+ # factory
39
+ "ToolName",
40
+ "create_agent_tools",
41
+ "create_worker_agent_tools",
42
+ # mcp
43
+ "create_mcp_tools",
44
+ "load_mcp_servers",
45
+ # planning
46
+ "create_planning_tools",
47
+ # skills
48
+ "create_skill_tools",
49
+ # user_interaction
50
+ "create_user_interaction_tools",
51
+ # web
52
+ "create_web_tools",
53
+ # workers
54
+ "WorkerTask",
55
+ "create_worker_tools",
56
+ # workspace
57
+ "create_workspace_tools",
58
+ ]