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