fix:优化图片识别,优化webui配置和排版,优化聊天流监控,新增mcp显示,新增prompt修改面板,优化插件状态显示,优化长期记忆控制台,
This commit is contained in:
@@ -91,7 +91,7 @@ def _should_refresh_image_component(component: ImageComponent) -> bool:
|
||||
"""判断图片组件当前是否仍处于待补全文本的占位状态。"""
|
||||
|
||||
normalized_content = component.content.strip()
|
||||
return not normalized_content or normalized_content == "[图片]"
|
||||
return not normalized_content or normalized_content == "[图片,识别中.....]"
|
||||
|
||||
|
||||
def _should_refresh_emoji_component(component: EmojiComponent) -> bool:
|
||||
|
||||
@@ -63,6 +63,7 @@ class ChatResponse:
|
||||
completion_tokens: int
|
||||
total_tokens: int
|
||||
prompt_section: Optional[RenderableType] = None
|
||||
prompt_html_uri: Optional[str] = None
|
||||
|
||||
|
||||
logger = get_logger("maisaka_chat_loop")
|
||||
@@ -585,8 +586,9 @@ class MaisakaChatLoopService:
|
||||
all_tools = [item for item in raw_tool_definitions if isinstance(item, dict)]
|
||||
|
||||
prompt_section: RenderableType | None = None
|
||||
prompt_html_uri: str | None = None
|
||||
if global_config.debug.show_maisaka_thinking:
|
||||
prompt_section = PromptCLIVisualizer.build_prompt_section(
|
||||
prompt_section_result = PromptCLIVisualizer.build_prompt_section_result(
|
||||
built_messages,
|
||||
category="planner" if request_kind != "timing_gate" else "timing_gate",
|
||||
chat_id=self._session_id,
|
||||
@@ -595,6 +597,9 @@ class MaisakaChatLoopService:
|
||||
folded=global_config.debug.fold_maisaka_thinking,
|
||||
tool_definitions=list(all_tools),
|
||||
)
|
||||
prompt_section = prompt_section_result.panel
|
||||
if prompt_section_result.preview_access is not None:
|
||||
prompt_html_uri = prompt_section_result.preview_access.viewer_web_uri
|
||||
|
||||
llm_chat = self._get_llm_chat_client(request_kind)
|
||||
generation_result = await llm_chat.generate_response_with_messages(
|
||||
@@ -660,6 +665,7 @@ class MaisakaChatLoopService:
|
||||
completion_tokens=completion_tokens,
|
||||
total_tokens=total_tokens,
|
||||
prompt_section=prompt_section,
|
||||
prompt_html_uri=prompt_html_uri,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -83,7 +83,7 @@ def _append_image_component(
|
||||
builder.add_text_content(normalized_content)
|
||||
return True
|
||||
|
||||
builder.add_text_content("[图片]")
|
||||
builder.add_text_content("[图片,识别中.....]")
|
||||
return True
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ def _render_component_for_prompt(component: StandardMessageComponents) -> str:
|
||||
return (component.text or "").strip()
|
||||
|
||||
if isinstance(component, ImageComponent):
|
||||
return component.content.strip() if component.content else "[图片]"
|
||||
return component.content.strip() if component.content else "[图片,识别中.....]"
|
||||
|
||||
if isinstance(component, EmojiComponent):
|
||||
return component.content.strip() if component.content else "[表情包]"
|
||||
|
||||
@@ -7,6 +7,7 @@ from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Literal
|
||||
from urllib.parse import quote
|
||||
|
||||
import hashlib
|
||||
import html
|
||||
@@ -32,6 +33,36 @@ from .prompt_preview_logger import PromptPreviewLogger
|
||||
DATA_IMAGE_DIR = REPO_ROOT / "data" / "images"
|
||||
|
||||
|
||||
def _build_prompt_preview_web_uri(file_path: Path) -> str:
|
||||
"""构建 WebUI 可访问的 Prompt 预览地址。"""
|
||||
|
||||
try:
|
||||
relative_path = file_path.resolve().relative_to(PromptPreviewLogger._BASE_DIR.resolve())
|
||||
except ValueError:
|
||||
return build_file_uri(file_path)
|
||||
return f"/api/webui/config/maisaka-prompt-preview?path={quote(relative_path.as_posix(), safe='')}"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PromptPreviewAccess:
|
||||
"""Prompt 预览文件的展示入口和可直接打开的路径。"""
|
||||
|
||||
body: RenderableType
|
||||
viewer_path: Path
|
||||
viewer_uri: str
|
||||
viewer_web_uri: str
|
||||
dump_path: Path
|
||||
dump_uri: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PromptSectionResult:
|
||||
"""Prompt 面板及其可选 HTML 预览入口。"""
|
||||
|
||||
panel: Panel
|
||||
preview_access: PromptPreviewAccess | None = None
|
||||
|
||||
|
||||
class PromptImageDisplayMode(str, Enum):
|
||||
"""图片在终端中的展示模式。"""
|
||||
|
||||
@@ -470,6 +501,77 @@ class PromptCLIVisualizer:
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def build_prompt_preview_access(
|
||||
cls,
|
||||
messages: list[Any],
|
||||
*,
|
||||
category: str,
|
||||
chat_id: str,
|
||||
request_kind: str,
|
||||
selection_reason: str,
|
||||
tool_definitions: list[dict[str, Any]] | None = None,
|
||||
) -> PromptPreviewAccess:
|
||||
"""保存 Prompt 预览文件,并返回 CLI 展示入口与浏览器可打开的 URI。"""
|
||||
|
||||
viewer_messages: list[dict[str, Any]] = []
|
||||
for message in messages:
|
||||
if isinstance(message, dict):
|
||||
viewer_messages.append(dict(message))
|
||||
continue
|
||||
|
||||
normalized_message = {
|
||||
"content": getattr(message, "content", None),
|
||||
"role": getattr(getattr(message, "role", "unknown"), "value", getattr(message, "role", "unknown")),
|
||||
}
|
||||
tool_call_id = getattr(message, "tool_call_id", None)
|
||||
if tool_call_id:
|
||||
normalized_message["tool_call_id"] = tool_call_id
|
||||
|
||||
tool_calls = getattr(message, "tool_calls", None)
|
||||
if tool_calls:
|
||||
normalized_message["tool_calls"] = [
|
||||
cls.format_tool_call_for_display(tool_call) for tool_call in tool_calls
|
||||
]
|
||||
viewer_messages.append(normalized_message)
|
||||
|
||||
prompt_dump_text = cls._build_prompt_dump_text(messages)
|
||||
tool_definition_dump_text = cls._build_tool_definition_dump_text(tool_definitions)
|
||||
if tool_definition_dump_text:
|
||||
prompt_dump_text = f"{prompt_dump_text}\n\n{'=' * 80}\n\n{tool_definition_dump_text}"
|
||||
viewer_html_text = cls._build_prompt_viewer_html(
|
||||
viewer_messages,
|
||||
request_kind=request_kind,
|
||||
selection_reason=selection_reason,
|
||||
tool_definitions=tool_definitions,
|
||||
)
|
||||
saved_paths = PromptPreviewLogger.save_preview_files(
|
||||
chat_id,
|
||||
category,
|
||||
{
|
||||
".html": viewer_html_text,
|
||||
".txt": prompt_dump_text,
|
||||
},
|
||||
)
|
||||
viewer_html_path = saved_paths[".html"]
|
||||
prompt_dump_path = saved_paths[".txt"]
|
||||
body = cls._build_preview_access_body(
|
||||
viewer_label="html预览",
|
||||
viewer_path=viewer_html_path,
|
||||
viewer_link_text="在浏览器打开 Prompt",
|
||||
dump_label="原始文本",
|
||||
dump_path=prompt_dump_path,
|
||||
dump_link_text="点击打开 Prompt 文本",
|
||||
)
|
||||
return PromptPreviewAccess(
|
||||
body=body,
|
||||
viewer_path=viewer_html_path,
|
||||
viewer_uri=build_file_uri(viewer_html_path),
|
||||
viewer_web_uri=_build_prompt_preview_web_uri(viewer_html_path),
|
||||
dump_path=prompt_dump_path,
|
||||
dump_uri=build_file_uri(prompt_dump_path),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _build_html_role_class(cls, role: str) -> str:
|
||||
return {
|
||||
@@ -804,56 +906,14 @@ class PromptCLIVisualizer:
|
||||
) -> RenderableType:
|
||||
"""构建用于查看完整 prompt 的折叠入口内容。"""
|
||||
|
||||
viewer_messages: list[dict[str, Any]] = []
|
||||
for message in messages:
|
||||
if isinstance(message, dict):
|
||||
viewer_messages.append(dict(message))
|
||||
continue
|
||||
|
||||
normalized_message = {
|
||||
"content": getattr(message, "content", None),
|
||||
"role": getattr(getattr(message, "role", "unknown"), "value", getattr(message, "role", "unknown")),
|
||||
}
|
||||
tool_call_id = getattr(message, "tool_call_id", None)
|
||||
if tool_call_id:
|
||||
normalized_message["tool_call_id"] = tool_call_id
|
||||
|
||||
tool_calls = getattr(message, "tool_calls", None)
|
||||
if tool_calls:
|
||||
normalized_message["tool_calls"] = [
|
||||
cls.format_tool_call_for_display(tool_call) for tool_call in tool_calls
|
||||
]
|
||||
viewer_messages.append(normalized_message)
|
||||
|
||||
prompt_dump_text = cls._build_prompt_dump_text(messages)
|
||||
tool_definition_dump_text = cls._build_tool_definition_dump_text(tool_definitions)
|
||||
if tool_definition_dump_text:
|
||||
prompt_dump_text = f"{prompt_dump_text}\n\n{'=' * 80}\n\n{tool_definition_dump_text}"
|
||||
viewer_html_text = cls._build_prompt_viewer_html(
|
||||
viewer_messages,
|
||||
return cls.build_prompt_preview_access(
|
||||
messages,
|
||||
category=category,
|
||||
chat_id=chat_id,
|
||||
request_kind=request_kind,
|
||||
selection_reason=selection_reason,
|
||||
tool_definitions=tool_definitions,
|
||||
)
|
||||
saved_paths = PromptPreviewLogger.save_preview_files(
|
||||
chat_id,
|
||||
category,
|
||||
{
|
||||
".html": viewer_html_text,
|
||||
".txt": prompt_dump_text,
|
||||
},
|
||||
)
|
||||
viewer_html_path = saved_paths[".html"]
|
||||
prompt_dump_path = saved_paths[".txt"]
|
||||
body = cls._build_preview_access_body(
|
||||
viewer_label="html预览",
|
||||
viewer_path=viewer_html_path,
|
||||
viewer_link_text="在浏览器打开 Prompt",
|
||||
dump_label="原始文本",
|
||||
dump_path=prompt_dump_path,
|
||||
dump_link_text="点击打开 Prompt 文本",
|
||||
)
|
||||
return body
|
||||
).body
|
||||
|
||||
@classmethod
|
||||
def build_prompt_section(
|
||||
@@ -870,26 +930,56 @@ class PromptCLIVisualizer:
|
||||
) -> Panel:
|
||||
"""构建用于嵌入结果面板中的 Prompt 区块。"""
|
||||
|
||||
return cls.build_prompt_section_result(
|
||||
messages,
|
||||
category=category,
|
||||
chat_id=chat_id,
|
||||
request_kind=request_kind,
|
||||
selection_reason=selection_reason,
|
||||
image_display_mode=image_display_mode,
|
||||
folded=folded,
|
||||
tool_definitions=tool_definitions,
|
||||
).panel
|
||||
|
||||
@classmethod
|
||||
def build_prompt_section_result(
|
||||
cls,
|
||||
messages: list[Any],
|
||||
*,
|
||||
category: str,
|
||||
chat_id: str,
|
||||
request_kind: str,
|
||||
selection_reason: str,
|
||||
image_display_mode: Literal["legacy", "path_link"] = "path_link",
|
||||
folded: bool,
|
||||
tool_definitions: list[dict[str, Any]] | None = None,
|
||||
) -> PromptSectionResult:
|
||||
"""构建 Prompt 面板,并在折叠模式下返回对应的 HTML 预览入口。"""
|
||||
|
||||
panel_title, panel_border_style = cls.get_request_panel_style(request_kind)
|
||||
preview_access = cls.build_prompt_preview_access(
|
||||
messages,
|
||||
category=category,
|
||||
chat_id=chat_id,
|
||||
request_kind=request_kind,
|
||||
selection_reason=selection_reason,
|
||||
tool_definitions=tool_definitions,
|
||||
)
|
||||
if folded:
|
||||
prompt_renderable = cls.build_prompt_access_panel(
|
||||
messages,
|
||||
category=category,
|
||||
chat_id=chat_id,
|
||||
request_kind=request_kind,
|
||||
selection_reason=selection_reason,
|
||||
tool_definitions=tool_definitions,
|
||||
)
|
||||
prompt_renderable = preview_access.body
|
||||
else:
|
||||
ordered_panels = cls.build_prompt_panels(messages)
|
||||
prompt_renderable = Group(*ordered_panels)
|
||||
|
||||
return Panel(
|
||||
prompt_renderable,
|
||||
title=panel_title,
|
||||
subtitle=selection_reason,
|
||||
border_style=panel_border_style,
|
||||
padding=(0, 1),
|
||||
return PromptSectionResult(
|
||||
panel=Panel(
|
||||
prompt_renderable,
|
||||
title=panel_title,
|
||||
subtitle=selection_reason,
|
||||
border_style=panel_border_style,
|
||||
padding=(0, 1),
|
||||
),
|
||||
preview_access=preview_access,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -95,7 +95,7 @@ def build_visible_text_from_sequence(message_sequence: MessageSequence) -> str:
|
||||
continue
|
||||
|
||||
if isinstance(component, ImageComponent):
|
||||
append_visible_part(component.content.strip() or "[图片]")
|
||||
append_visible_part(component.content.strip() or "[图片,识别中.....]")
|
||||
continue
|
||||
|
||||
if isinstance(component, AtComponent):
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
import json
|
||||
import time
|
||||
|
||||
from src.common.logger import get_logger
|
||||
|
||||
@@ -57,7 +58,7 @@ def _extract_text_content(content: Any) -> Optional[str]:
|
||||
if block_type == "text":
|
||||
text_parts.append(str(block.get("text", "")))
|
||||
elif block_type == "image_url":
|
||||
text_parts.append("[图片]")
|
||||
text_parts.append("[图片,识别中.....]")
|
||||
else:
|
||||
text_parts.append(f"[{block_type}]")
|
||||
elif isinstance(block, str):
|
||||
@@ -66,43 +67,65 @@ def _extract_text_content(content: Any) -> Optional[str]:
|
||||
return str(content)
|
||||
|
||||
|
||||
def _normalize_tool_call_arguments(arguments: Any) -> tuple[Any, Optional[str]]:
|
||||
"""标准化工具调用参数,兼容 JSON 字符串和对象。"""
|
||||
|
||||
if isinstance(arguments, str):
|
||||
raw_arguments = arguments
|
||||
try:
|
||||
parsed_arguments = json.loads(arguments) if arguments.strip() else {}
|
||||
except json.JSONDecodeError:
|
||||
return {}, raw_arguments
|
||||
return _normalize_payload_value(parsed_arguments), raw_arguments
|
||||
return _normalize_payload_value(arguments or {}), None
|
||||
|
||||
|
||||
def _serialize_single_tool_call(tool_call: Any) -> Dict[str, Any]:
|
||||
"""将不同来源的 tool_call 标准化为前端可直接展示的结构。"""
|
||||
|
||||
if isinstance(tool_call, dict):
|
||||
function_info = tool_call.get("function")
|
||||
if isinstance(function_info, dict):
|
||||
raw_arguments = function_info.get("arguments", tool_call.get("arguments", tool_call.get("args", {})))
|
||||
name = function_info.get("name", tool_call.get("name", tool_call.get("func_name", "unknown")))
|
||||
else:
|
||||
raw_arguments = tool_call.get("arguments", tool_call.get("args", {}))
|
||||
name = tool_call.get("name", tool_call.get("func_name", "unknown"))
|
||||
|
||||
arguments, arguments_raw = _normalize_tool_call_arguments(raw_arguments)
|
||||
serialized: Dict[str, Any] = {
|
||||
"id": str(tool_call.get("id", tool_call.get("call_id", ""))),
|
||||
"name": str(name or "unknown"),
|
||||
"arguments": arguments,
|
||||
}
|
||||
if arguments_raw is not None:
|
||||
serialized["arguments_raw"] = arguments_raw
|
||||
return serialized
|
||||
|
||||
raw_arguments = getattr(tool_call, "args", None)
|
||||
if raw_arguments is None:
|
||||
raw_arguments = getattr(tool_call, "arguments", None)
|
||||
arguments, arguments_raw = _normalize_tool_call_arguments(raw_arguments)
|
||||
serialized = {
|
||||
"id": str(getattr(tool_call, "id", None) or getattr(tool_call, "call_id", "")),
|
||||
"name": str(getattr(tool_call, "func_name", None) or getattr(tool_call, "name", "unknown")),
|
||||
"arguments": arguments,
|
||||
}
|
||||
if arguments_raw is not None:
|
||||
serialized["arguments_raw"] = arguments_raw
|
||||
return serialized
|
||||
|
||||
|
||||
def _serialize_tool_calls_from_objects(tool_calls: List[Any]) -> List[Dict[str, Any]]:
|
||||
"""将工具调用对象列表序列化为字典列表。"""
|
||||
|
||||
result: List[Dict[str, Any]] = []
|
||||
for tool_call in tool_calls:
|
||||
serialized: Dict[str, Any] = {
|
||||
"id": getattr(tool_call, "id", None) or getattr(tool_call, "call_id", ""),
|
||||
"name": getattr(tool_call, "func_name", None) or getattr(tool_call, "name", "unknown"),
|
||||
}
|
||||
args = getattr(tool_call, "args", None) or getattr(tool_call, "arguments", None)
|
||||
if isinstance(args, dict):
|
||||
serialized["arguments"] = _normalize_payload_value(args)
|
||||
elif isinstance(args, str):
|
||||
serialized["arguments_raw"] = args
|
||||
result.append(serialized)
|
||||
return result
|
||||
return [_serialize_single_tool_call(tool_call) for tool_call in tool_calls]
|
||||
|
||||
|
||||
def _serialize_tool_calls_from_dicts(tool_calls: List[Any]) -> List[Dict[str, Any]]:
|
||||
"""将工具调用字典列表标准化为可传输格式。"""
|
||||
|
||||
result: List[Dict[str, Any]] = []
|
||||
for tool_call in tool_calls:
|
||||
if isinstance(tool_call, dict):
|
||||
result.append({
|
||||
"id": str(tool_call.get("id", "")),
|
||||
"name": str(tool_call.get("name", tool_call.get("func_name", "unknown"))),
|
||||
"arguments": _normalize_payload_value(tool_call.get("arguments", tool_call.get("args", {}))),
|
||||
})
|
||||
continue
|
||||
|
||||
result.append({
|
||||
"id": str(getattr(tool_call, "id", getattr(tool_call, "call_id", ""))),
|
||||
"name": str(getattr(tool_call, "func_name", getattr(tool_call, "name", "unknown"))),
|
||||
"arguments": _normalize_payload_value(getattr(tool_call, "args", getattr(tool_call, "arguments", {}))),
|
||||
})
|
||||
return result
|
||||
return [_serialize_single_tool_call(tool_call) for tool_call in tool_calls]
|
||||
|
||||
|
||||
def _serialize_message(message: Any) -> Dict[str, Any]:
|
||||
@@ -214,6 +237,7 @@ def _serialize_planner_block(
|
||||
completion_tokens: Optional[int],
|
||||
total_tokens: Optional[int],
|
||||
duration_ms: Optional[float],
|
||||
prompt_html_uri: Optional[str] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""标准化 planner 结果区块。"""
|
||||
|
||||
@@ -224,6 +248,7 @@ def _serialize_planner_block(
|
||||
and completion_tokens is None
|
||||
and total_tokens is None
|
||||
and duration_ms is None
|
||||
and prompt_html_uri is None
|
||||
):
|
||||
return None
|
||||
|
||||
@@ -234,6 +259,7 @@ def _serialize_planner_block(
|
||||
"completion_tokens": int(completion_tokens or 0),
|
||||
"total_tokens": int(total_tokens or 0),
|
||||
"duration_ms": float(duration_ms or 0.0),
|
||||
"prompt_html_uri": str(prompt_html_uri or ""),
|
||||
}
|
||||
|
||||
|
||||
@@ -429,6 +455,7 @@ async def emit_planner_finalized(
|
||||
planner_completion_tokens: Optional[int],
|
||||
planner_total_tokens: Optional[int],
|
||||
planner_duration_ms: Optional[float],
|
||||
planner_prompt_html_uri: Optional[str],
|
||||
tools: Optional[List[Dict[str, Any]]],
|
||||
time_records: Dict[str, float],
|
||||
agent_state: str,
|
||||
@@ -464,6 +491,7 @@ async def emit_planner_finalized(
|
||||
planner_completion_tokens,
|
||||
planner_total_tokens,
|
||||
planner_duration_ms,
|
||||
planner_prompt_html_uri,
|
||||
),
|
||||
"tools": _serialize_tool_results(list(tools or [])),
|
||||
"final_state": {
|
||||
|
||||
@@ -709,6 +709,7 @@ class MaisakaReasoningEngine:
|
||||
),
|
||||
planner_total_tokens=response.total_tokens if response is not None else None,
|
||||
planner_duration_ms=planner_duration_ms if response is not None else None,
|
||||
planner_prompt_html_uri=response.prompt_html_uri if response is not None else None,
|
||||
tools=tool_monitor_results,
|
||||
time_records=dict(completed_cycle.time_records),
|
||||
agent_state=self._runtime._agent_state,
|
||||
|
||||
Reference in New Issue
Block a user