refs #732: polish HMM report title and response
This commit is contained in:
@@ -2886,7 +2886,71 @@ def _normalize_presentation_value(value: Any) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
def _presentation_payload(question: str, steps: list[Mapping[str, Any]]) -> dict[str, Any]:
|
||||
def _clean_presentation_title(value: Any) -> str:
|
||||
text = html.unescape(re.sub(r"<[^>]+>", " ", str(value or "")))
|
||||
text = " ".join(text.split()).strip(" \t\r\n\"'`-–—:;,.!?·")
|
||||
if len(text) > 48:
|
||||
text = text[:47].rstrip() + "…"
|
||||
return text or "업무 현황"
|
||||
|
||||
|
||||
def _plan_presentation_title(
|
||||
*,
|
||||
question: str,
|
||||
source_step: Mapping[str, Any],
|
||||
model_profile_key: str,
|
||||
) -> str:
|
||||
"""Generate a concise business title without coupling to a specific report."""
|
||||
|
||||
fallback = "업무 현황"
|
||||
try:
|
||||
profile = resolve_model_profile(model_profile_key)
|
||||
client = build_oci_genai_completion_client(
|
||||
profile.model_id,
|
||||
profile.answer_model_region,
|
||||
profile.answer_model_endpoint,
|
||||
)
|
||||
text = client.complete(
|
||||
system_prompt=(
|
||||
"Create one concise Korean enterprise report heading. Preserve the "
|
||||
"business subject, team/user scope, and identifiers. Remove output-format "
|
||||
"and action phrases such as HTML, report, dashboard, 보여줘, 만들어줘. "
|
||||
"Use a noun phrase, not a sentence. The template separately appends a fixed "
|
||||
"subtitle, so do not include '최신 선사 실적'. Return only schema JSON."
|
||||
),
|
||||
user_prompt=json.dumps(
|
||||
{
|
||||
"question": question,
|
||||
"source_tool": str(source_step.get("tool_name") or ""),
|
||||
"source_rows": len(
|
||||
_mcp_structured_rows(source_step.get("mcp_result", {}))
|
||||
),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
response_schema={
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"required": ["title"],
|
||||
"properties": {"title": {"type": "string"}},
|
||||
},
|
||||
max_tokens=120,
|
||||
temperature=temperature_for_model_profile(profile),
|
||||
)
|
||||
parsed = json.loads(text)
|
||||
if isinstance(parsed, Mapping):
|
||||
return _clean_presentation_title(parsed.get("title"))
|
||||
except Exception:
|
||||
pass
|
||||
return fallback
|
||||
|
||||
|
||||
def _presentation_payload(
|
||||
question: str,
|
||||
steps: list[Mapping[str, Any]],
|
||||
*,
|
||||
title: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""Build a renderer payload only from the preceding authorized tool result."""
|
||||
|
||||
source = next(
|
||||
@@ -2910,7 +2974,7 @@ def _presentation_payload(question: str, steps: list[Mapping[str, Any]]) -> dict
|
||||
"report": {
|
||||
"id": "MCP-REPORT",
|
||||
"category": "MCP Business Intelligence",
|
||||
"title": str(question or "MCP 결과 리포트"),
|
||||
"title": _clean_presentation_title(title),
|
||||
"question": str(question or ""),
|
||||
"requestedBy": requester.group(0).upper() if requester else "-",
|
||||
"generatedAt": datetime.now(timezone.utc).isoformat(),
|
||||
@@ -2937,6 +3001,7 @@ def _build_mcp_arguments(
|
||||
limit: int,
|
||||
preferred_tool: str,
|
||||
steps: list[Mapping[str, Any]],
|
||||
presentation_title: str = "",
|
||||
) -> dict[str, Any]:
|
||||
if not _is_presentation_route(route):
|
||||
return build_mcp_tool_arguments(
|
||||
@@ -2964,12 +3029,63 @@ def _build_mcp_arguments(
|
||||
raise McpToolRouterError("렌더링 MCP tool이 report payload 입력을 선언하지 않았습니다.")
|
||||
return {
|
||||
input_name: json.dumps(
|
||||
_presentation_payload(tool_query, steps),
|
||||
_presentation_payload(
|
||||
tool_query,
|
||||
steps,
|
||||
title=presentation_title,
|
||||
),
|
||||
ensure_ascii=False,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def _presentation_artifact_summary(
|
||||
mcp_result: Any,
|
||||
agent_steps: list[Mapping[str, Any]] | None,
|
||||
) -> dict[str, Any]:
|
||||
if not _mcp_rendered_html(mcp_result):
|
||||
return {}
|
||||
title = "업무 현황"
|
||||
row_count = 0
|
||||
for step in reversed(agent_steps or []):
|
||||
if not isinstance(step, Mapping):
|
||||
continue
|
||||
if not bool(step.get("consumes_previous_result")):
|
||||
row_count = len(_mcp_structured_rows(step.get("mcp_result", {})))
|
||||
continue
|
||||
arguments = step.get("arguments")
|
||||
if not isinstance(arguments, Mapping):
|
||||
continue
|
||||
for raw_payload in arguments.values():
|
||||
if not isinstance(raw_payload, str):
|
||||
continue
|
||||
try:
|
||||
parsed = json.loads(raw_payload)
|
||||
except ValueError:
|
||||
continue
|
||||
report = parsed.get("report") if isinstance(parsed, Mapping) else None
|
||||
if isinstance(report, Mapping):
|
||||
title = _clean_presentation_title(report.get("title"))
|
||||
rows = parsed.get("rows")
|
||||
if isinstance(rows, list):
|
||||
row_count = len(rows)
|
||||
break
|
||||
return {"title": title, "row_count": row_count}
|
||||
|
||||
|
||||
def _presentation_completion_answer(
|
||||
mcp_result: Any,
|
||||
agent_steps: list[Mapping[str, Any]] | None,
|
||||
) -> str:
|
||||
artifact = _presentation_artifact_summary(mcp_result, agent_steps)
|
||||
if not artifact:
|
||||
return ""
|
||||
return (
|
||||
f"**{artifact['title']}** 리포트를 생성했습니다. "
|
||||
f"아래 생성된 리포트에서 조회 결과 {artifact['row_count']}건을 확인할 수 있습니다."
|
||||
)
|
||||
|
||||
|
||||
def _mcp_has_actionable_result(mcp_result: Any) -> bool:
|
||||
if _mcp_generated_sql(mcp_result) or _mcp_items(mcp_result):
|
||||
return True
|
||||
@@ -3878,12 +3994,28 @@ def run_mcp_agent_loop(
|
||||
raise PublicMcpError("선택된 MCP 서버 설정을 찾지 못했습니다.")
|
||||
|
||||
started = perf_counter()
|
||||
presentation_title = ""
|
||||
if _is_presentation_route(route):
|
||||
source_step = next(
|
||||
(
|
||||
step
|
||||
for step in reversed(steps)
|
||||
if not bool(step.get("consumes_previous_result"))
|
||||
),
|
||||
{},
|
||||
)
|
||||
presentation_title = _plan_presentation_title(
|
||||
question=question,
|
||||
source_step=source_step,
|
||||
model_profile_key=model_profile_key,
|
||||
)
|
||||
arguments = _build_mcp_arguments(
|
||||
route=route,
|
||||
tool_query=question if _is_presentation_route(route) else tool_query,
|
||||
limit=int(limit),
|
||||
preferred_tool=server.default_tool,
|
||||
steps=steps,
|
||||
presentation_title=presentation_title,
|
||||
)
|
||||
raw_result = call_tool(
|
||||
base_url=server.endpoint_url,
|
||||
@@ -4526,6 +4658,10 @@ def synthesize_answer(
|
||||
}
|
||||
for step in (agent_steps or [])
|
||||
],
|
||||
"presentation_artifact": _presentation_artifact_summary(
|
||||
mcp_result,
|
||||
agent_steps,
|
||||
),
|
||||
}
|
||||
return json.dumps(prompt_payload, ensure_ascii=False)
|
||||
|
||||
@@ -4609,7 +4745,11 @@ def synthesize_answer(
|
||||
"Show audit_log_id when it is provided. Follow every required_answer_checks item in the user "
|
||||
"payload. When the user requests a result list, include every "
|
||||
"row provided in the MCP evidence, up to 20 rows, and state the total "
|
||||
"returned row count. Do not arbitrarily stop at five rows. Keep the "
|
||||
"returned row count. Do not arbitrarily stop at five rows. If "
|
||||
"presentation_artifact is present, the HTML artifact is the final visual output: "
|
||||
"do not reproduce HTML tags, Markdown tables, or individual data rows in answer. "
|
||||
"Only confirm the artifact title and row count and direct the user to the rendered "
|
||||
"report displayed below. Keep the "
|
||||
"answer concise and business-readable."
|
||||
)
|
||||
for attempt in range(1, 3):
|
||||
@@ -4909,8 +5049,18 @@ def _render_assistant_message(
|
||||
message: Mapping[str, Any],
|
||||
message_key: str,
|
||||
) -> None:
|
||||
st.markdown(str(message.get("content") or ""))
|
||||
details = message.get("details")
|
||||
content = str(message.get("content") or "")
|
||||
if isinstance(details, Mapping):
|
||||
presentation_answer = _presentation_completion_answer(
|
||||
details.get("mcp_result", {}),
|
||||
details.get("agent_steps")
|
||||
if isinstance(details.get("agent_steps"), list)
|
||||
else None,
|
||||
)
|
||||
if presentation_answer:
|
||||
content = presentation_answer
|
||||
st.markdown(content)
|
||||
basis = message.get("basis")
|
||||
if isinstance(basis, list) and basis:
|
||||
display_records = _display_markdown_records(
|
||||
@@ -5647,6 +5797,13 @@ def _process_submitted_question(
|
||||
}
|
||||
refresh_progress(96, "원본 MCP 응답으로 결과를 구성했습니다.")
|
||||
|
||||
presentation_answer = _presentation_completion_answer(
|
||||
answer_source,
|
||||
agent_steps,
|
||||
)
|
||||
if presentation_answer:
|
||||
assistant_message["content"] = presentation_answer
|
||||
|
||||
pre_save_elapsed = perf_counter() - process_started
|
||||
record_execution_event(
|
||||
percent=98,
|
||||
|
||||
Reference in New Issue
Block a user