fix(tui): add plain text format support to session export command
CI / lint (pull_request) Successful in 21s
CI / typecheck (pull_request) Successful in 57s
CI / security (pull_request) Successful in 1m4s
CI / quality (pull_request) Successful in 35s
CI / build (pull_request) Successful in 18s
CI / helm (pull_request) Successful in 23s
CI / unit_tests (pull_request) Successful in 7m0s
CI / e2e_tests (pull_request) Successful in 15m25s
CI / integration_tests (pull_request) Successful in 23m13s
CI / coverage (pull_request) Successful in 11m18s
CI / docker (pull_request) Successful in 1m29s
CI / status-check (pull_request) Successful in 2s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Successful in 56m57s
CI / lint (pull_request) Successful in 21s
CI / typecheck (pull_request) Successful in 57s
CI / security (pull_request) Successful in 1m4s
CI / quality (pull_request) Successful in 35s
CI / build (pull_request) Successful in 18s
CI / helm (pull_request) Successful in 23s
CI / unit_tests (pull_request) Successful in 7m0s
CI / e2e_tests (pull_request) Successful in 15m25s
CI / integration_tests (pull_request) Successful in 23m13s
CI / coverage (pull_request) Successful in 11m18s
CI / docker (pull_request) Successful in 1m29s
CI / status-check (pull_request) Successful in 2s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Successful in 56m57s
Add plain text (txt) export format to the TUI /session:export command. - Add Session.as_export_plain_text() domain method that produces a human-readable plain text transcript without Markdown formatting - Update TuiCommandRouter._session_export() to accept 'txt' as a valid format, building the plain text content via as_export_plain_text() - Update the invalid-format error message to include 'txt' as a valid option alongside 'json' and 'md' - Add BDD scenarios covering plain text export via TUI command and domain model, including with-messages and no-messages cases - Add corresponding step definitions for the new BDD scenarios The plain text format uses a simple separator-based layout: Session: <id> Actor: <actor> ... ---------------------------------------- [0] USER (timestamp): message content ---------------------------------------- ISSUES CLOSED: #3036
This commit is contained in:
@@ -399,3 +399,41 @@ def step_tui_file_exists(context: Context, path: str) -> None:
|
||||
# Cleanup
|
||||
with contextlib.suppress(OSError):
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Domain model: as_export_plain_text()
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given("a session with two messages for plain text export")
|
||||
def step_session_two_messages_plain(context: Context) -> None:
|
||||
msgs = [
|
||||
_make_message(MessageRole.USER, "Hello from user", 0),
|
||||
_make_message(MessageRole.ASSISTANT, "Hello from assistant", 1),
|
||||
]
|
||||
context.txt_session = _make_session(messages=msgs)
|
||||
|
||||
|
||||
@given("a session with no messages for plain text export")
|
||||
def step_session_no_messages_plain(context: Context) -> None:
|
||||
context.txt_session = _make_session()
|
||||
|
||||
|
||||
@when("I call as_export_plain_text on the session")
|
||||
def step_call_as_export_plain_text(context: Context) -> None:
|
||||
context.txt_output = context.txt_session.as_export_plain_text()
|
||||
|
||||
|
||||
@then('the plain text output should start with "{prefix}"')
|
||||
def step_txt_starts_with(context: Context, prefix: str) -> None:
|
||||
assert context.txt_output.startswith(prefix), (
|
||||
f"Expected plain text to start with {prefix!r}, got: {context.txt_output[:80]!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the plain text output should contain "{text}"')
|
||||
def step_txt_contains(context: Context, text: str) -> None:
|
||||
assert text in context.txt_output, (
|
||||
f"Expected {text!r} in plain text output.\nGot:\n{context.txt_output[:500]}"
|
||||
)
|
||||
|
||||
@@ -110,3 +110,35 @@ Feature: TUI session export/import (JSON + Markdown)
|
||||
And there is an invalid JSON file for TUI import
|
||||
When I call TUI handle with "session import <invalid_path>" for the import file
|
||||
Then the TUI handle result should contain "Invalid JSON"
|
||||
|
||||
Scenario: TUI session export with --format txt returns success
|
||||
Given a TUI command router with mocked session service for export
|
||||
When I call TUI handle with "session export --format txt" for the current session
|
||||
Then the TUI handle result should contain "Session exported (TXT)"
|
||||
|
||||
Scenario: TUI session export with --format txt to file writes plain text
|
||||
Given a TUI command router with mocked session service for export
|
||||
When I call TUI handle with "session export --format txt /tmp/tui_test_export.txt" for the current session
|
||||
Then the TUI handle result should contain "Session exported to"
|
||||
And the tui exported file "/tmp/tui_test_export.txt" should exist
|
||||
|
||||
Scenario: Plain text export contains session header and messages
|
||||
Given a session with two messages for plain text export
|
||||
When I call as_export_plain_text on the session
|
||||
Then the plain text output should start with "Session:"
|
||||
And the plain text output should contain "USER"
|
||||
And the plain text output should contain "ASSISTANT"
|
||||
And the plain text output should contain "Hello from user"
|
||||
And the plain text output should contain "Hello from assistant"
|
||||
|
||||
Scenario: Plain text export with no messages contains placeholder
|
||||
Given a session with no messages for plain text export
|
||||
When I call as_export_plain_text on the session
|
||||
Then the plain text output should start with "Session:"
|
||||
And the plain text output should contain "(no messages)"
|
||||
|
||||
Scenario: TUI session export error message includes txt format
|
||||
Given a TUI command router with mocked session service for export
|
||||
When I call TUI handle with "session export --format xml" for the current session
|
||||
Then the TUI handle result should contain "Invalid format"
|
||||
And the TUI handle result should contain "txt"
|
||||
|
||||
@@ -467,6 +467,54 @@ class Session(BaseModel):
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
def as_export_plain_text(self) -> str:
|
||||
"""Return a plain text transcript of the session.
|
||||
|
||||
This is a **lossy** export intended for sharing and reading in plain
|
||||
text editors. It does not contain enough information to fully restore
|
||||
a session via ``import_session`` -- use :meth:`as_export_dict` for that.
|
||||
|
||||
The output format is::
|
||||
|
||||
Session: <session_id>
|
||||
Actor: <actor_name>
|
||||
Namespace: <namespace>
|
||||
Created: <created_at>
|
||||
Updated: <updated_at>
|
||||
|
||||
----------------------------------------
|
||||
|
||||
[<sequence>] <ROLE> (<timestamp>):
|
||||
<content>
|
||||
|
||||
----------------------------------------
|
||||
"""
|
||||
separator = "-" * 40
|
||||
lines: list[str] = []
|
||||
lines.append(f"Session: {self.session_id}")
|
||||
lines.append(f"Actor: {self.actor_name or '(none)'}")
|
||||
lines.append(f"Namespace: {self.namespace}")
|
||||
lines.append(f"Created: {self.created_at.isoformat()}")
|
||||
lines.append(f"Updated: {self.updated_at.isoformat()}")
|
||||
if self.linked_plan_ids:
|
||||
plan_refs = ", ".join(self.linked_plan_ids)
|
||||
lines.append(f"Linked Plans: {plan_refs}")
|
||||
lines.append("")
|
||||
lines.append(separator)
|
||||
lines.append("")
|
||||
if not self.messages:
|
||||
lines.append("(no messages)")
|
||||
lines.append("")
|
||||
else:
|
||||
for msg in self.messages:
|
||||
ts = msg.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
||||
lines.append(f"[{msg.sequence}] {msg.role.value.upper()} ({ts}):")
|
||||
lines.append(msg.content)
|
||||
lines.append("")
|
||||
lines.append(separator)
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
model_config = ConfigDict(
|
||||
str_strip_whitespace=True,
|
||||
validate_assignment=True,
|
||||
|
||||
@@ -95,8 +95,8 @@ class TuiCommandRouter:
|
||||
path = tokens[i]
|
||||
i += 1
|
||||
|
||||
if fmt not in ("json", "md"):
|
||||
return f"Invalid format: {fmt!r}. Use 'json' or 'md'."
|
||||
if fmt not in ("json", "md", "txt"):
|
||||
return f"Invalid format: {fmt!r}. Use 'json', 'md', or 'txt'."
|
||||
|
||||
try:
|
||||
container = self._resolve_container()
|
||||
@@ -124,6 +124,27 @@ class TuiCommandRouter:
|
||||
]
|
||||
session.messages = messages
|
||||
content = session.as_export_markdown()
|
||||
elif fmt == "txt":
|
||||
from cleveragents.domain.models.core.session import (
|
||||
MessageRole,
|
||||
SessionMessage,
|
||||
)
|
||||
|
||||
session = service.get(session_id)
|
||||
messages = [
|
||||
SessionMessage(
|
||||
message_id=m["message_id"],
|
||||
role=MessageRole(m["role"]),
|
||||
content=m["content"],
|
||||
sequence=m["sequence"],
|
||||
timestamp=m["timestamp"],
|
||||
metadata=m.get("metadata", {}),
|
||||
tool_call_id=m.get("tool_call_id"),
|
||||
)
|
||||
for m in export_data.get("messages", [])
|
||||
]
|
||||
session.messages = messages
|
||||
content = session.as_export_plain_text()
|
||||
else:
|
||||
content = json.dumps(export_data, indent=2, default=str)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user