refs #732: polish HMM report title and response

This commit is contained in:
devmrko
2026-08-10 16:06:17 +09:00
parent b13b913939
commit 66fcb5d149
6 changed files with 247 additions and 5 deletions

View File

@@ -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,