refs #732: scope report titles to selected user

This commit is contained in:
devmrko
2026-08-10 18:02:49 +09:00
parent 84029ed633
commit cbd49b6b05
6 changed files with 225 additions and 6 deletions

View File

@@ -443,17 +443,28 @@ def _is_failed_synthesis_answer(value: object) -> bool:
)
def load_chat_context(conversation_id: str) -> list[dict[str, str]]:
def load_chat_context(
conversation_id: str,
*,
selected_user_id: str = "",
) -> list[dict[str, str]]:
current_user_id = str(selected_user_id or "").strip()
with _chat_db_connect() as connection:
rows = connection.execute(
"""
SELECT question, answer
FROM poc4_mcp_chat_turns
WHERE conversation_id = ?
AND (? = '' OR selected_user_id = ?)
ORDER BY turn_id DESC
LIMIT ?
""",
(conversation_id, CHAT_CONTEXT_TURNS),
(
conversation_id,
current_user_id,
current_user_id,
CHAT_CONTEXT_TURNS,
),
).fetchall()
messages: list[dict[str, str]] = []
for row in reversed(rows):
@@ -2997,6 +3008,7 @@ def _plan_presentation_title(
question: str,
source_step: Mapping[str, Any],
model_profile_key: str,
selected_user_id: str = "",
) -> str:
"""Generate a concise business title without coupling to a specific report."""
@@ -3013,12 +3025,17 @@ def _plan_presentation_title(
"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, 보여줘, 만들어줘. "
"The current selected user ID is authoritative. First-person expressions "
"such as 내, 나, and 우리 refer to that current user, never to a user from "
"earlier conversation turns. Do not change the current user into a manager "
"or another employee unless the latest question explicitly names that scope. "
"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,
"current_selected_user_id": str(selected_user_id or "").strip(),
"source_tool": str(source_step.get("tool_name") or ""),
"source_rows": len(
_mcp_structured_rows(source_step.get("mcp_result", {}))
@@ -3048,6 +3065,7 @@ def _presentation_payload(
steps: list[Mapping[str, Any]],
*,
title: str = "",
selected_user_id: str = "",
) -> dict[str, Any]:
"""Build a renderer payload only from the preceding authorized tool result."""
@@ -3067,6 +3085,9 @@ def _presentation_payload(
if isinstance(row, Mapping)
]
requester = re.search(r"\bE\d{4,}\b", str(question or ""), flags=re.IGNORECASE)
requested_by = str(selected_user_id or "").strip().upper()
if not requested_by and requester:
requested_by = requester.group(0).upper()
source_tool = str(source.get("tool_name") or "MCP data query")
return {
"report": {
@@ -3074,7 +3095,7 @@ def _presentation_payload(
"category": "MCP Business Intelligence",
"title": _clean_presentation_title(title),
"question": str(question or ""),
"requestedBy": requester.group(0).upper() if requester else "-",
"requestedBy": requested_by or "-",
"generatedAt": datetime.now(timezone.utc).isoformat(),
"answer": (
f"{source_tool}에서 권한 범위 내 결과 {len(rows)}건을 받아 "
@@ -3100,6 +3121,7 @@ def _build_mcp_arguments(
preferred_tool: str,
steps: list[Mapping[str, Any]],
presentation_title: str = "",
selected_user_id: str = "",
) -> dict[str, Any]:
if not _is_presentation_route(route):
return build_mcp_tool_arguments(
@@ -3131,6 +3153,7 @@ def _build_mcp_arguments(
tool_query,
steps,
title=presentation_title,
selected_user_id=selected_user_id,
),
ensure_ascii=False,
)
@@ -3892,6 +3915,7 @@ def run_mcp_agent_loop(
bearer_token: str,
limit: int,
model_profile_key: str,
selected_user_id: str = "",
initial_route_key: str = "",
progress_callback: Any = None,
) -> dict[str, Any]:
@@ -4112,6 +4136,7 @@ def run_mcp_agent_loop(
question=question,
source_step=source_step,
model_profile_key=model_profile_key,
selected_user_id=selected_user_id,
)
arguments = _build_mcp_arguments(
route=route,
@@ -4120,6 +4145,7 @@ def run_mcp_agent_loop(
preferred_tool=server.default_tool,
steps=steps,
presentation_title=presentation_title,
selected_user_id=selected_user_id,
)
raw_result = call_tool(
base_url=server.endpoint_url,
@@ -4324,9 +4350,11 @@ def resolve_standalone_question(
question: str,
messages: list[Mapping[str, Any]],
model_profile_key: str,
selected_user_id: str = "",
) -> str:
context = _conversation_context(messages)
if not context:
current_user_id = str(selected_user_id or "").strip()
if not context and not current_user_id:
return question
try:
profile = resolve_model_profile(model_profile_key)
@@ -4339,10 +4367,19 @@ def resolve_standalone_question(
system_prompt=(
"Rewrite the user's latest Korean question into one standalone "
"MCP tool query. Use the conversation only to resolve references. "
"The current selected user ID is authoritative. First-person expressions "
"such as 내, 나, and 우리 refer only to the current selected user. Never "
"reuse another user's identity, manager scope, or team scope from earlier "
"conversation turns. If the latest question is self-contained, preserve its "
"meaning and scope. "
"Do not answer the question."
),
user_prompt=json.dumps(
{"conversation": context, "latest_question": question},
{
"current_selected_user_id": current_user_id,
"conversation": context,
"latest_question": question,
},
ensure_ascii=False,
),
response_schema={
@@ -5454,7 +5491,15 @@ def _process_submitted_question(
with processing_status:
refresh_progress(5, "대화 문맥을 불러오고 있습니다.")
step_started = perf_counter()
conversation = load_chat_context(conversation_id)
current_selected_user_id = (
selected_token_preset.user_id
if selected_token_preset is not None
else ""
)
conversation = load_chat_context(
conversation_id,
selected_user_id=current_selected_user_id,
)
step_elapsed = perf_counter() - step_started
refresh_progress(
15,
@@ -5469,6 +5514,7 @@ def _process_submitted_question(
question=normalized_question,
messages=conversation,
model_profile_key=active_model_profile,
selected_user_id=current_selected_user_id,
)
step_elapsed = perf_counter() - step_started
refresh_progress(
@@ -5591,6 +5637,7 @@ def _process_submitted_question(
bearer_token=bearer_token,
limit=int(limit),
model_profile_key=reasoning_model_profile,
selected_user_id=current_selected_user_id,
initial_route_key=_route_key(
selected_route.server_id,
selected_route.tool.name,