expose shell metadata in call responses

This commit is contained in:
lda
2026-08-30 03:05:58 +07:00 Verified
parent cf0ec7b403
commit 2587b07b4a
6 changed files with 90 additions and 6 deletions
-1
View File
@@ -11,7 +11,6 @@ dependencies = [
"ipython>=9.16.1", "ipython>=9.16.1",
"matplotlib>=3.11.1", "matplotlib>=3.11.1",
"matplotlib-inline>=0.1.7", "matplotlib-inline>=0.1.7",
"sympy>=1.14.0",
"wonderwords>=2.2.0", "wonderwords>=2.2.0",
] ]
+11 -1
View File
@@ -6,7 +6,15 @@ from .events import (
event_from_execution_result, event_from_execution_result,
event_from_history_output, event_from_history_output,
) )
from .models import CallError, CallEvent, CallEventKind, CallResponse, CallResult from .models import (
CallError,
CallEvent,
CallEventKind,
CallResponse,
CallResult,
ShellInfo,
ShellStatus,
)
from .shell import ( from .shell import (
Shell, Shell,
isolate_output_history, isolate_output_history,
@@ -26,6 +34,8 @@ __all__ = [
"Shell", "Shell",
"event_from_history_output", "event_from_history_output",
"generate_good_names", "generate_good_names",
"ShellInfo",
"ShellStatus",
"isolate_output_history", "isolate_output_history",
"run_cell_and_collect", "run_cell_and_collect",
"setup_shell", "setup_shell",
+6 -1
View File
@@ -6,7 +6,7 @@ from uuid import uuid4
from wonderwords import RandomWord from wonderwords import RandomWord
from .models import CallResponse from .models import CallResponse, ShellInfo
from .shell import Shell from .shell import Shell
@@ -50,6 +50,10 @@ class App:
self.last_shell = shell_name self.last_shell = shell_name
return shell_name, self.shells[shell_name] return shell_name, self.shells[shell_name]
def list_shells(self) -> list[ShellInfo]:
"""Return frontend-safe metadata for all known shells."""
return [shell.describe() for shell in self.shells.values()]
def run_code( def run_code(
self, self,
code: str, code: str,
@@ -68,6 +72,7 @@ class App:
collapsed=collapsed, collapsed=collapsed,
result=result, result=result,
events=events, events=events,
shell=shell.describe(),
) )
+16
View File
@@ -12,6 +12,8 @@ CallEventKind = Literal[
"error", "error",
] ]
ShellStatus = Literal["ready", "running", "error"]
@dataclass @dataclass
class CallEvent: class CallEvent:
@@ -55,3 +57,17 @@ class CallResponse:
collapsed: bool collapsed: bool
result: CallResult result: CallResult
events: list[CallEvent] events: list[CallEvent]
shell: ShellInfo
@dataclass
class ShellInfo:
"""Frontend-safe snapshot describing one persistent shell."""
shell_id: str
name: str
status: ShellStatus
execution_count: int
created_at: str
last_used_at: str | None
capabilities: tuple[str, ...]
+43 -3
View File
@@ -1,4 +1,6 @@
from collections import defaultdict from collections import defaultdict
from datetime import datetime, timezone
from uuid import uuid4
from IPython.core.interactiveshell import InteractiveShell from IPython.core.interactiveshell import InteractiveShell
from IPython.core.pylabtools import ( from IPython.core.pylabtools import (
@@ -14,7 +16,7 @@ from .events import (
event_from_execution_result, event_from_execution_result,
event_from_history_output, event_from_history_output,
) )
from .models import CallEvent, CallResult from .models import CallEvent, CallResult, ShellInfo, ShellStatus
class Shell: class Shell:
@@ -22,6 +24,11 @@ class Shell:
def __init__(self, name: str): def __init__(self, name: str):
self.name = name self.name = name
self.shell_id = uuid4().hex
self.created_at = datetime.now(timezone.utc).isoformat()
self.last_used_at: str | None = None
self.status: ShellStatus = "ready"
self._last_execution_count = 0
self._shell: InteractiveShell | None = None self._shell: InteractiveShell | None = None
@property @property
@@ -45,7 +52,40 @@ class Shell:
call_id: str, call_id: str,
) -> tuple[CallResult, list[CallEvent]]: ) -> tuple[CallResult, list[CallEvent]]:
"""Execute one cell and return its result plus ordered output events.""" """Execute one cell and return its result plus ordered output events."""
return run_cell_and_collect(self.init_shell(), code, call_id=call_id) self.status = "running"
try:
call_result, events = run_cell_and_collect(
self.init_shell(),
code,
call_id=call_id,
)
except Exception:
self.status = "error"
raise
else:
self.status = "ready"
self.last_used_at = datetime.now(timezone.utc).isoformat()
self._last_execution_count = call_result.execution_count or 0
return call_result, events
def describe(self) -> ShellInfo:
"""Return metadata without initializing an unused shell."""
return ShellInfo(
shell_id=self.shell_id,
name=self.name,
status=self.status,
execution_count=self._last_execution_count,
created_at=self.created_at,
last_used_at=self.last_used_at,
capabilities=(
"stdout",
"stderr",
"display_data",
"execute_result",
"error",
"matplotlib-inline",
),
)
def isolate_output_history(shell: InteractiveShell) -> None: def isolate_output_history(shell: InteractiveShell) -> None:
@@ -76,7 +116,7 @@ def run_cell_and_collect(
execution = shell.run_cell(code, store_history=True) execution = shell.run_cell(code, store_history=True)
records = tuple( records = tuple(
history_manager.outputs.get(execution.execution_count, ()) history_manager.outputs.get(execution.execution_count, ())
) ) if execution.execution_count is not None else ()
events = [ events = [
event_from_history_output( event_from_history_output(
record, record,
+14
View File
@@ -73,6 +73,20 @@ class AppTests(unittest.TestCase):
self.assertNotEqual(first.call_id, second.call_id) self.assertNotEqual(first.call_id, second.call_id)
self.assertEqual(second.result.result, 42) self.assertEqual(second.result.result, 42)
def test_response_and_shell_list_expose_shell_info(self):
app = App()
with contextlib.redirect_stdout(io.StringIO()):
call = app.run_code("1 + 1", shell_name="new")
shells = app.list_shells()
self.assertEqual(len(shells), 1)
self.assertEqual(call.shell.shell_id, shells[0].shell_id)
self.assertEqual(call.shell.name, call.shell_name)
self.assertEqual(call.shell.execution_count, call.result.execution_count)
self.assertIn("execute_result", call.shell.capabilities)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()