Add human-readable labels to chat tool calls for UI rendering.
- `ChatToolCallInfo` 타입에 `label` 필드를 추가하여 클라이언트가 도구 호출을 직관적인 칩 형태로 표시할 수 있도록 개선했습니다. - `chatToolService`에 도구별 라벨 매핑 테이블(`TOOL_LABELS`)과 이를 적용하는 `withToolLabels` 함수를 구현하여, API 응답 시점에만 라벨이 동적으로 결합되도록 설계했습니다. - 기존 저장소(Firestore) 로직에는 영향을 주지 않으면서, 메시지 조회 및 전송 결과 반환 시점에 라벨을 주입하여 표현 계층의 유연성을 확보했습니다. - 테스트 코드를 수정하여 도구 호출 응답 및 이력 조회 시 라벨이 정확하게 포함되는지 검증했습니다.
This commit is contained in:
parent
e466676210
commit
5f519afcbf
@ -31,7 +31,7 @@ import { checkInput, checkOutput, crisisReply, detectCrisis } from "./chatFilter
|
||||
import { logChatEvent } from "./chatAnalyticsService";
|
||||
import { extractNavActions } from "./chatNavService";
|
||||
import { assemblePrompt, gatherUserContext, resolveTeamCode, type UserContext } from "./chatContextService";
|
||||
import { buildChatTools } from "./chatToolService";
|
||||
import { buildChatTools, withToolLabels } from "./chatToolService";
|
||||
import { FALLBACK_SUGGESTION_IDS, FILTERED_REPLY, INPUT_BLOCKED_NOTICE } from "../constants/chatPrompts";
|
||||
import { CHAT_REPORT_REASONS, type ChatConfig, type ChatMessagesPage, type ChatMessageView,
|
||||
type ChatQuotaView, type ChatReportReason, type ChatSendResult, type ChatSuggestion,
|
||||
@ -100,7 +100,7 @@ async function buildSendResult(
|
||||
remainingCount: Math.max(0, config.dailyLimit - used),
|
||||
limit: config.dailyLimit,
|
||||
createdAt: toKstIso(createdAt),
|
||||
...(toolCalls && toolCalls.length > 0 ? { toolCalls } : {}),
|
||||
...(toolCalls && toolCalls.length > 0 ? { toolCalls: withToolLabels(toolCalls) } : {}),
|
||||
...(actions && actions.length > 0 ? { actions } : {}),
|
||||
};
|
||||
}
|
||||
@ -465,7 +465,7 @@ export async function getMessages(
|
||||
crisis: m.crisis,
|
||||
createdAt: toKstIso(m.createdAt),
|
||||
...(m.role === "user" && m.clientMessageId ? { clientMessageId: m.clientMessageId } : {}),
|
||||
...(m.role === "assistant" && m.toolCalls?.length ? { toolCalls: m.toolCalls } : {}),
|
||||
...(m.role === "assistant" && m.toolCalls?.length ? { toolCalls: withToolLabels(m.toolCalls) } : {}),
|
||||
...(m.role === "assistant" && m.actions?.length ? { actions: m.actions } : {}),
|
||||
}));
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import { getMonthDoc } from "../repositories/attendanceRepository";
|
||||
import { getLatestBalance } from "../repositories/pointLedgerRepository";
|
||||
import { TEAM_DISPLAY_NAMES, KBO_RANK_TEAM_NAMES } from "../constants/chatPrompts";
|
||||
import type { ChatTool } from "./chatProviderService";
|
||||
import type { ChatToolCallInfo } from "../types/chat";
|
||||
import type { Lineup, KeyPlayerRanking } from "../kbo/game-detail";
|
||||
import type { PlayerRecord } from "../kbo/player/common";
|
||||
import type { TeamRank } from "../kbo/team-rank";
|
||||
@ -15,6 +16,27 @@ import type { ScheduleGame } from "../types/kbo";
|
||||
import { TeamCode, type AttendanceMonthDoc, type VoteHistoryDoc } from "../types/panit";
|
||||
import { addDaysKst, type DateString } from "../types/dateString";
|
||||
|
||||
/**
|
||||
* 도구 이름 → 사람이 읽을 라벨. 클라이언트가 "🔍 순위 조회" 같은 칩으로 표시하기 위함.
|
||||
* 저장(Firestore)은 이름·인자만, 라벨은 API 응답 시점에만 부여한다(표현 계층, 변경 자유).
|
||||
*/
|
||||
const TOOL_LABELS: Record<string, string> = {
|
||||
get_lineup: "라인업 조회",
|
||||
get_roster: "로스터 조회",
|
||||
get_game_standouts: "경기 활약 선수 조회",
|
||||
get_team_rank_snapshot: "순위 조회",
|
||||
get_my_attendance_this_month: "내 출석·포인트 조회",
|
||||
get_prediction_breakdown_date: "내 예측 내역 조회",
|
||||
};
|
||||
|
||||
/** toolCalls에 사람이 읽을 라벨을 부여한다(매핑 없는 도구는 라벨 생략). API 경계에서만 호출. */
|
||||
export function withToolLabels(calls: ChatToolCallInfo[]): ChatToolCallInfo[] {
|
||||
return calls.map((c) => {
|
||||
const label = TOOL_LABELS[c.name];
|
||||
return label ? { ...c, label } : c;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 채팅 도구(함수 호출) 레이어 — "질문 시점에만 알 수 있는" 정보를 모델이
|
||||
* 필요할 때만 DB/KBO에서 조회하도록 한다(§5 컨텍스트 상시 주입의 보완).
|
||||
|
||||
@ -18,6 +18,8 @@ export type ChatRole = "user" | "assistant";
|
||||
export interface ChatToolCallInfo {
|
||||
name: string;
|
||||
args: Record<string, unknown>;
|
||||
/** 사람이 읽을 도구 라벨(예: "순위 조회"). API 응답에서만 부여 — 저장은 안 함. 매핑 없으면 생략. */
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -176,8 +176,10 @@ describe("chatService", () => {
|
||||
expect((await readQuota()).used).toBe(1);
|
||||
});
|
||||
|
||||
it("도구 호출 메타(이름+인자)를 응답과 이력에 노출한다", async () => {
|
||||
it("도구 호출 메타(이름+인자+사람이 읽을 라벨)를 응답과 이력에 노출한다", async () => {
|
||||
const calls = [{ name: "get_lineup", args: { date: "2026-05-01" } }];
|
||||
// API 경계에서 도구 이름에 사람이 읽을 라벨이 부여된다.
|
||||
const labeled = [{ name: "get_lineup", args: { date: "2026-05-01" }, label: "라인업 조회" }];
|
||||
setTestProvider({
|
||||
complete: async () => ({
|
||||
reply: "오늘 선발은 김민준이야 짹",
|
||||
@ -187,12 +189,12 @@ describe("chatService", () => {
|
||||
}),
|
||||
});
|
||||
const result = await sendMessage(uid, { message: "오늘 선발 누구?", clientMessageId: newUuid() });
|
||||
expect(result.toolCalls).toEqual(calls);
|
||||
expect(result.toolCalls).toEqual(labeled);
|
||||
|
||||
// GET 이력의 assistant 메시지에도 toolCalls가 보인다(나중에 클라 활용)
|
||||
const page = await getMessages(uid, undefined, undefined);
|
||||
const assistant = page.messages.find((m) => m.role === "assistant");
|
||||
expect(assistant?.toolCalls).toEqual(calls);
|
||||
expect(assistant?.toolCalls).toEqual(labeled);
|
||||
});
|
||||
|
||||
it("출력이 교체(crisis/filtered)되면 도구 메타를 붙이지 않는다", async () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user