feat: enhance chat CLI and TUI with initial opener handling and improved prompt logic

This commit is contained in:
Xin Wang
2026-03-10 23:55:53 +08:00
parent ef2614a70a
commit a55ca37c39
5 changed files with 278 additions and 26 deletions

View File

@@ -32,6 +32,7 @@ load_dotenv(Path(__file__).with_name(".env"))
API_KEY = os.getenv("API_KEY")
BASE_URL = os.getenv("BASE_URL")
APP_ID = os.getenv("APP_ID")
for stream in (sys.stdout, sys.stderr):
if hasattr(stream, "reconfigure"):
@@ -144,16 +145,85 @@ def _resolve_option_token(token: str, options: List[Dict[str, str]]) -> Optional
return None
def _coerce_text(value: Any) -> str:
return str(value or "").strip()
def _first_nonempty_text(*values: Any) -> str:
for value in values:
text = _coerce_text(value)
if text:
return text
return ""
def _merge_prompt_parts(*values: Any) -> str:
parts: List[str] = []
seen = set()
for value in values:
text = _coerce_text(value)
if not text or text in seen:
continue
seen.add(text)
parts.append(text)
return "\n".join(parts)
def _interactive_prompt_text(payload: Dict[str, Any], default_text: str) -> str:
params = payload.get("params") if isinstance(payload.get("params"), dict) else {}
return str(
payload.get("prompt")
or payload.get("title")
or payload.get("text")
or payload.get("description")
or params.get("description")
or default_text
).strip()
opener = _first_nonempty_text(
payload.get("opener"),
params.get("opener"),
payload.get("intro"),
params.get("intro"),
)
prompt = _first_nonempty_text(
payload.get("prompt"),
params.get("prompt"),
payload.get("text"),
params.get("text"),
)
title = _first_nonempty_text(
payload.get("title"),
params.get("title"),
payload.get("nodeName"),
payload.get("label"),
)
description = _first_nonempty_text(
payload.get("description"),
payload.get("desc"),
params.get("description"),
params.get("desc"),
)
return _merge_prompt_parts(opener, prompt) or title or description or default_text
def _extract_chat_init_opener(payload: Any) -> str:
if not isinstance(payload, dict):
return ""
data = payload.get("data") if isinstance(payload.get("data"), dict) else payload
app = data.get("app") if isinstance(data.get("app"), dict) else {}
chat_config = app.get("chatConfig") if isinstance(app.get("chatConfig"), dict) else {}
return _first_nonempty_text(
chat_config.get("welcomeText"),
app.get("welcomeText"),
data.get("welcomeText"),
data.get("opener"),
app.get("opener"),
data.get("intro"),
app.get("intro"),
)
def _get_initial_app_opener(client: ChatClient, chat_id: str) -> str:
if not APP_ID:
return ""
response = client.get_chat_init(appId=APP_ID, chatId=chat_id)
response.raise_for_status()
return _extract_chat_init_opener(response.json())
def _prompt_user_select(event: FastGPTInteractiveEvent) -> Optional[str]:
@@ -165,7 +235,8 @@ def _prompt_user_select(event: FastGPTInteractiveEvent) -> Optional[str]:
options = [item for index, raw in enumerate(raw_options, start=1) if (item := _normalize_option(raw, index))]
print()
print(f"[INTERACTIVE] {prompt_text}")
print("[INTERACTIVE]")
print(prompt_text)
for index, option in enumerate(options, start=1):
print(f" {index}. {option['label']}")
if option["description"]:
@@ -212,7 +283,8 @@ def _prompt_user_input(event: FastGPTInteractiveEvent) -> Optional[str]:
form_fields = params.get("inputForm") if isinstance(params.get("inputForm"), list) else []
print()
print(f"[INTERACTIVE] {prompt_text}")
print("[INTERACTIVE]")
print(prompt_text)
if not form_fields:
value = input("Input (/cancel to stop): ").strip()
@@ -384,6 +456,15 @@ def main() -> None:
print(f"Using chatId: {chat_id}\n")
with ChatClient(api_key=API_KEY, base_url=BASE_URL) as client:
try:
opener = _get_initial_app_opener(client, chat_id)
except Exception as exc:
print(f"[INIT] Failed to load app opener: {exc}\n")
opener = ""
if opener:
print(f"Assistant: {opener}\n")
while True:
try:
user_input = input("You: ").strip()

View File

@@ -32,8 +32,10 @@ if str(EXAMPLES_DIR) not in sys.path:
sys.path.insert(0, str(EXAMPLES_DIR))
from chat_cli import (
APP_ID,
API_KEY,
BASE_URL,
_extract_chat_init_opener,
_extract_text_from_event,
_interactive_prompt_text,
_normalize_option,
@@ -451,10 +453,11 @@ class FastGPTWorkbench(App[None]):
raise RuntimeError("Set API_KEY and BASE_URL in examples/.env before starting chat_tui.py")
self._refresh_sidebar()
self._set_status("Ready", "Fresh session")
initial_message = self._initial_session_message()
self._append_message(
role="system",
title="Session",
content="Start typing below. FastGPT workflow events will appear in the left rail.",
content=initial_message,
)
self.query_one("#composer", TextArea).focus()
@@ -464,8 +467,9 @@ class FastGPTWorkbench(App[None]):
def _refresh_sidebar(self) -> None:
session_panel = self.query_one("#session_panel", Static)
base_url = BASE_URL or ""
app_id = APP_ID or "(not set)"
session_panel.update(
f"Session\n\nchatId: {self.chat_id}\nbaseUrl: {base_url}"
f"Session\n\nchatId: {self.chat_id}\nappId: {app_id}\nbaseUrl: {base_url}"
)
def _set_status(self, heading: str, detail: str) -> None:
@@ -481,6 +485,24 @@ class FastGPTWorkbench(App[None]):
except Exception:
return content
def _default_session_message(self) -> str:
return "Start typing below. FastGPT workflow events will appear in the left rail."
def _initial_session_message(self) -> str:
if not APP_ID:
return self._default_session_message()
try:
with ChatClient(api_key=API_KEY, base_url=BASE_URL) as client:
response = client.get_chat_init(appId=APP_ID, chatId=self.chat_id)
response.raise_for_status()
opener = _extract_chat_init_opener(response.json())
except Exception as exc:
self._log_event(f"[init] Failed to load app opener: {exc}")
return self._default_session_message()
return opener or self._default_session_message()
def _append_message(self, role: str, title: str, content: str) -> str:
self._message_counter += 1
widget_id = f"message-{self._message_counter}"
@@ -546,7 +568,8 @@ class FastGPTWorkbench(App[None]):
self._start_turn(result, title="Workflow Input", role="workflow")
def _present_interactive(self, event: FastGPTInteractiveEvent) -> None:
self._log_event(f"[interactive] {event.interaction_type}")
prompt_summary = _interactive_prompt_text(event.data, event.interaction_type).replace("\n", " / ")
self._log_event(f"[interactive] {event.interaction_type}: {prompt_summary}")
if event.interaction_type == "userInput":
self.push_screen(InteractiveInputScreen(event), self._handle_interactive_result)
return
@@ -569,10 +592,11 @@ class FastGPTWorkbench(App[None]):
self.query_one("#messages", VerticalScroll).remove_children()
self._refresh_sidebar()
self._set_status("Ready", "Started a new random session")
initial_message = self._initial_session_message()
self._append_message(
role="system",
title="Session",
content="New chat created. Start typing below.",
content=initial_message,
)
self._log_event(f"[local] Started new chatId {self.chat_id}")