Use realistic personas for cached demo agents

This commit is contained in:
liyizhouAI 2026-06-08 19:37:57 +08:00
parent c5cc5912c0
commit e1467b53e2
2 changed files with 144 additions and 46 deletions

View File

@ -56,6 +56,84 @@ CASE_AGENTS = [
]
CASE_AGENT_PERSONAS = {
0: {"display_name": "周明远", "title": "宁波银行宁波分行大客户经理", "organization": "宁波银行宁波分行", "persona_role": "银行客户经理", "age": 39, "mbti": "ENTJ"},
1: {"display_name": "陈启航", "title": "舟山精密制造企业董事长", "organization": "启航精密制造集团", "persona_role": "高净值客户 / 制造业企业主", "age": 56, "mbti": "ISTJ"},
2: {"display_name": "何婉清", "title": "宁波银行私人银行投资顾问", "organization": "宁波银行私人银行中心", "persona_role": "私行投资顾问", "age": 36, "mbti": "ENFJ"},
3: {"display_name": "林泽宇", "title": "科技主题基金投资经理", "organization": "华东科技成长基金", "persona_role": "科技基金经理", "age": 41, "mbti": "INTJ"},
4: {"display_name": "蒋亦辰", "title": "宁波银行固收产品经理", "organization": "宁波银行资产管理部", "persona_role": "固收产品经理", "age": 38, "mbti": "ISTJ"},
5: {"display_name": "郑雅宁", "title": "宁波银行财富合规风控经理", "organization": "宁波银行财富合规部", "persona_role": "风控合规经理", "age": 42, "mbti": "ISTJ"},
6: {"display_name": "罗景行", "title": "AI产业链首席研究员", "organization": "甬江产业研究院", "persona_role": "AI行业研究员", "age": 37, "mbti": "INTP"},
7: {"display_name": "顾澜", "title": "陈启航家族办公室顾问", "organization": "启航家族办公室", "persona_role": "家族办公室代表", "age": 45, "mbti": "INFJ"},
8: {"display_name": "沈雨薇", "title": "陈启航配偶 / 家庭共同决策人", "organization": "启航家庭资产委员会", "persona_role": "家庭共同决策人", "age": 53, "mbti": "ISFJ"},
9: {"display_name": "陈知远", "title": "家族企业数字化负责人", "organization": "启航精密制造集团", "persona_role": "二代继承人", "age": 31, "mbti": "ENTP"},
10: {"display_name": "赵立衡", "title": "舟山精密制造财务总监", "organization": "启航精密制造集团", "persona_role": "企业财务负责人", "age": 47, "mbti": "ISTJ"},
11: {"display_name": "孙若谷", "title": "宁波银行分行财富管理部负责人", "organization": "宁波银行宁波分行", "persona_role": "分行财富主管", "age": 44, "mbti": "ESTJ"},
12: {"display_name": "胡静姝", "title": "宁波银行总行产品准入经理", "organization": "宁波银行总行财富产品部", "persona_role": "总行产品准入经理", "age": 40, "mbti": "ISTJ"},
13: {"display_name": "唐可", "title": "宁波银行运营留痕专员", "organization": "宁波银行运营支持中心", "persona_role": "运营留痕专员", "age": 32, "mbti": "ISFJ"},
14: {"display_name": "许安澜", "title": "宁波银行私人银行客户服务经理", "organization": "宁波银行私人银行中心", "persona_role": "客户服务经理", "age": 34, "mbti": "ESFJ"},
15: {"display_name": "马承骁", "title": "量化对冲产品经理", "organization": "东方量化资产管理", "persona_role": "量化对冲产品经理", "age": 40, "mbti": "INTJ"},
16: {"display_name": "金若宁", "title": "黄金与多资产策略师", "organization": "甬江多资产研究中心", "persona_role": "多资产策略师", "age": 35, "mbti": "ENFP"},
17: {"display_name": "邵砚", "title": "半导体设备行业研究员", "organization": "华东证券研究所", "persona_role": "半导体研究员", "age": 34, "mbti": "INTP"},
18: {"display_name": "陆晨曦", "title": "云计算与企业软件研究员", "organization": "甬江产业研究院", "persona_role": "云计算研究员", "age": 33, "mbti": "ENTJ"},
19: {"display_name": "谢知衡", "title": "数据中心基础设施研究员", "organization": "长三角基础设施研究中心", "persona_role": "数据中心研究员", "age": 36, "mbti": "INTJ"},
20: {"display_name": "顾廷川", "title": "利率与信用策略师", "organization": "宁波银行金融市场部", "persona_role": "利率策略师", "age": 43, "mbti": "ISTJ"},
21: {"display_name": "方舒越", "title": "外汇与黄金策略师", "organization": "宁波银行金融市场部", "persona_role": "汇率策略师", "age": 37, "mbti": "ENTP"},
22: {"display_name": "俞博文", "title": "汽车零部件公司创始人", "organization": "博文汽车零部件", "persona_role": "同圈层高净值客户", "age": 54, "mbti": "ESTP"},
23: {"display_name": "戴敏", "title": "连锁药房股东 / 保守型高净值客户", "organization": "民安连锁药房", "persona_role": "保守型客户", "age": 58, "mbti": "ISFJ"},
24: {"display_name": "韩峥", "title": "互联网创业者 / 进取型高净值客户", "organization": "灵犀数据科技", "persona_role": "进取型客户", "age": 38, "mbti": "ENTP"},
25: {"display_name": "叶楚航", "title": "宁波商会副会长 / 客户朋友推荐人", "organization": "宁波青年企业家商会", "persona_role": "转介绍推荐人", "age": 49, "mbti": "ENFJ"},
26: {"display_name": "宋知夏", "title": "宁波银行合规质检员", "organization": "宁波银行财富合规部", "persona_role": "合规质检员", "age": 31, "mbti": "ISTJ"},
27: {"display_name": "魏清和", "title": "家族信托顾问", "organization": "甬信家族办公室服务中心", "persona_role": "家族信托顾问", "age": 46, "mbti": "INFJ"},
28: {"display_name": "乔若琳", "title": "保险金信托顾问", "organization": "甬诚保险经纪", "persona_role": "保险金信托顾问", "age": 39, "mbti": "ESFJ"},
29: {"display_name": "彭越", "title": "分行财富销售数据分析师", "organization": "宁波银行数据经营团队", "persona_role": "销售数据分析师", "age": 33, "mbti": "INTP"},
}
def _agent_persona(agent_id: int, fallback_name: str, fallback_role: str, fallback_bio: str) -> Dict[str, Any]:
persona = CASE_AGENT_PERSONAS.get(agent_id, {})
display_name = persona.get("display_name", fallback_name)
title = persona.get("title", fallback_role)
role_name = persona.get("persona_role", fallback_name)
organization = persona.get("organization", "宁波银行案例模拟世界")
return {
"display_name": display_name,
"title": title,
"organization": organization,
"persona_role": role_name,
"age": persona.get("age", 42),
"mbti": persona.get("mbti", "ISTJ"),
"bio": f"{display_name}{title},来自{organization}{fallback_bio}",
}
def _agent_profile_payload(agent_id: int, name: str, role: str, bio: str) -> Dict[str, Any]:
persona = _agent_persona(agent_id, name, role, bio)
agent_code = f"A{agent_id:02d}"
return {
"id": agent_id,
"agent_id": agent_id,
"name": agent_code,
"handle": agent_code,
"username": persona["display_name"],
"display_name": persona["display_name"],
"real_name": persona["display_name"],
"entity_name": name,
"entity_type": role,
"role": persona["persona_role"],
"title": persona["title"],
"profession": persona["title"],
"organization": persona["organization"],
"bio": persona["bio"],
"persona": f"{persona['display_name']}以“{persona['persona_role']}”身份参与本案例围绕高净值客户AI理财组合推介讨论资产配置、风险边界、服务动作和销售转化。",
"interested_topics": ["科技理财", "AI产业链", "资产配置", "合规销售"],
"age": persona["age"],
"gender": "other",
"country": "CN",
"mbti": persona["mbti"],
}
def _case_time(minutes: int = 0) -> str:
return (datetime.fromisoformat(NB_CREATED_AT) + timedelta(minutes=minutes)).isoformat()
@ -449,20 +527,7 @@ def get_cached_profiles(simulation_id: str, platform: str = "reddit") -> Optiona
profiles = []
for agent_id, name, role, bio in CASE_AGENTS:
profiles.append({
"id": agent_id,
"name": name,
"username": f"agent_{agent_id}",
"entity_type": role,
"profession": role,
"bio": bio,
"persona": f"{name}在本案例中围绕高净值客户AI理财组合推介参与讨论关注资产配置、风险边界和销售转化。",
"interested_topics": ["科技理财", "AI产业链", "资产配置", "合规销售"],
"age": 42 if agent_id in (0, 2, 5, 6, 7) else 55,
"gender": "other",
"country": "CN",
"mbti": "ENTJ" if agent_id in (0, 2) else "ISTJ",
})
profiles.append(_agent_profile_payload(agent_id, name, role, bio))
return profiles
@ -472,13 +537,17 @@ def get_cached_config(simulation_id: str) -> Optional[Dict[str, Any]]:
agent_configs = []
for agent_id, name, role, _ in CASE_AGENTS:
persona = _agent_persona(agent_id, name, role, "")
active_hours = list(range(9, 19)) if agent_id in (0, 2, 4, 5, 6, 11, 12, 13, 14, 26, 29) else list(range(10, 22))
agent_configs.append({
"agent_id": agent_id,
"name": name,
"name": persona["display_name"],
"display_name": persona["display_name"],
"entity_name": name,
"entity_type": role,
"role": role,
"role": persona["persona_role"],
"title": persona["title"],
"organization": persona["organization"],
"active_time_period": "09:00-18:00" if active_hours[0] == 9 else "10:00-21:00",
"active_hours": active_hours,
"posts_per_hour": 0.7 if agent_id in (0, 2, 6, 11, 29) else 0.35,
@ -1277,9 +1346,12 @@ def get_cached_infographic(report_id: str) -> Optional[Dict[str, Any]]:
by_type[action_type] = by_type.get(action_type, 0) + 1
by_platform[platform][action_type] = by_platform[platform].get(action_type, 0) + 1
agent = next((a for a in CASE_AGENTS if a[0] == agent_id), CASE_AGENTS[0])
persona = _agent_persona(agent_id, agent[1], agent[2], agent[3])
bucket = agent_counts.setdefault(agent_id, {
"agent_id": agent_id,
"agent_name": agent[1],
"agent_name": persona["display_name"],
"agent_title": persona["title"],
"entity_name": agent[1],
"total_actions": 0,
})
bucket["total_actions"] += 1
@ -1391,12 +1463,15 @@ ROUND_ACTIONS = [
def _action(agent_id: int, round_num: int, action_type: str, content: str, timestamp: str, platform: str = "reddit") -> Dict[str, Any]:
agent = next((a for a in CASE_AGENTS if a[0] == agent_id), CASE_AGENTS[0])
persona = _agent_persona(agent_id, agent[1], agent[2], agent[3])
return {
"round_num": round_num,
"timestamp": timestamp,
"platform": platform,
"agent_id": agent_id,
"agent_name": agent[1],
"agent_name": persona["display_name"],
"agent_title": persona["title"],
"entity_name": agent[1],
"action_type": action_type,
"action_args": {
"content": content,
@ -1522,14 +1597,7 @@ def get_cached_replay(simulation_id: str) -> Optional[Dict[str, Any]]:
"agent_configs_count": len(CASE_AGENTS),
},
"agents": [
{
"id": agent_id,
"name": name,
"username": f"agent_{agent_id}",
"profession": role,
"bio": bio,
"interested_topics": ["科技理财", "AI产业链", "资产配置", "合规销售"],
}
_agent_profile_payload(agent_id, name, role, bio)
for agent_id, name, role, bio in CASE_AGENTS
],
"rounds": rounds,

View File

@ -112,7 +112,7 @@
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
<span>{{ selectedAgent ? selectedAgent.username : $t('step5.chatWithAgent') }}</span>
<span>{{ selectedAgent ? getAgentDisplayName(selectedAgent) : $t('step5.chatWithAgent') }}</span>
<svg class="dropdown-arrow" :class="{ open: showAgentDropdown }" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
@ -125,10 +125,11 @@
class="dropdown-item"
@click="selectAgent(agent, idx)"
>
<div class="agent-avatar">{{ (agent.username || 'A')[0] }}</div>
<div class="agent-avatar">{{ getAgentInitial(agent) }}</div>
<div class="agent-info">
<span class="agent-name">{{ agent.username }}</span>
<span class="agent-role">{{ agent.profession || $t('step2.unknownProfession') }}</span>
<span class="agent-name">{{ getAgentDisplayName(agent) }}</span>
<span class="agent-role">{{ getAgentRoleLine(agent) }}</span>
<span class="agent-code">{{ getAgentHandle(agent, idx) }} · {{ agent.role || agent.entity_name || agent.entity_type }}</span>
</div>
</div>
</div>
@ -221,12 +222,12 @@
<!-- Agent Profile Card -->
<div v-if="chatTarget === 'agent' && selectedAgent" class="agent-profile-card">
<div class="profile-card-header">
<div class="profile-card-avatar">{{ (selectedAgent.username || 'A')[0] }}</div>
<div class="profile-card-avatar">{{ getAgentInitial(selectedAgent) }}</div>
<div class="profile-card-info">
<div class="profile-card-name">{{ selectedAgent.username }}</div>
<div class="profile-card-name">{{ getAgentDisplayName(selectedAgent) }}</div>
<div class="profile-card-meta">
<span v-if="selectedAgent.name" class="profile-card-handle">@{{ selectedAgent.name }}</span>
<span class="profile-card-profession">{{ selectedAgent.profession || $t('step2.unknownProfession') }}</span>
<span class="profile-card-handle">@{{ getAgentHandle(selectedAgent, selectedAgentIndex) }}</span>
<span class="profile-card-profession">{{ getAgentRoleLine(selectedAgent) }}</span>
</div>
</div>
<button class="profile-card-toggle" @click="showFullProfile = !showFullProfile">
@ -263,12 +264,12 @@
>
<div class="message-avatar">
<span v-if="msg.role === 'user'">U</span>
<span v-else>{{ msg.role === 'assistant' && chatTarget === 'report_agent' ? 'R' : (selectedAgent?.username?.[0] || 'A') }}</span>
<span v-else>{{ msg.role === 'assistant' && chatTarget === 'report_agent' ? 'R' : getAgentInitial(selectedAgent) }}</span>
</div>
<div class="message-content">
<div class="message-header">
<span class="sender-name">
{{ msg.role === 'user' ? 'You' : (chatTarget === 'report_agent' ? 'Report Agent' : (selectedAgent?.username || 'Agent')) }}
{{ msg.role === 'user' ? 'You' : (chatTarget === 'report_agent' ? 'Report Agent' : getAgentDisplayName(selectedAgent, 'Agent')) }}
</span>
<span class="message-time">{{ formatTime(msg.timestamp) }}</span>
</div>
@ -292,7 +293,7 @@
</div>
<div v-if="isSending" class="chat-message assistant">
<div class="message-avatar">
<span>{{ chatTarget === 'report_agent' ? 'R' : (selectedAgent?.username?.[0] || 'A') }}</span>
<span>{{ chatTarget === 'report_agent' ? 'R' : getAgentInitial(selectedAgent) }}</span>
</div>
<div class="message-content">
<div class="typing-indicator">
@ -349,10 +350,10 @@
:checked="selectedAgents.has(idx)"
@change="toggleAgentSelection(idx)"
>
<div class="checkbox-avatar">{{ (agent.username || 'A')[0] }}</div>
<div class="checkbox-avatar">{{ getAgentInitial(agent) }}</div>
<div class="checkbox-info">
<span class="checkbox-name">{{ agent.username }}</span>
<span class="checkbox-role">{{ agent.profession || $t('step2.unknownProfession') }}</span>
<span class="checkbox-name">{{ getAgentDisplayName(agent) }}</span>
<span class="checkbox-role">{{ getAgentRoleLine(agent) }}</span>
</div>
<div class="checkbox-indicator">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="3">
@ -468,6 +469,26 @@ const isSurveying = ref(false)
// Report Data
const reportOutline = ref(null)
const generatedSections = ref({})
const getAgentDisplayName = (agent, fallback = 'Agent') => {
return agent?.display_name || agent?.real_name || agent?.username || agent?.entity_name || agent?.name || fallback
}
const getAgentRoleLine = (agent) => {
return agent?.title || agent?.profession || agent?.role || agent?.entity_name || agent?.entity_type || t('step2.unknownProfession')
}
const getAgentHandle = (agent, idx = null) => {
if (agent?.handle) return agent.handle
if (typeof agent?.name === 'string' && /^A\d{2}$/.test(agent.name)) return agent.name
const id = agent?.agent_id ?? agent?.id ?? idx
if (id !== null && id !== undefined) return `A${String(id).padStart(2, '0')}`
return 'A--'
}
const getAgentInitial = (agent) => {
return (getAgentDisplayName(agent, 'A').trim()[0] || 'A').toUpperCase()
}
const collapsedSections = ref(new Set())
const currentSectionIndex = ref(null)
const highlightedSectionIndex = ref(null)
@ -660,7 +681,7 @@ const selectAgent = (agent, idx) => {
// Agent
chatHistory.value = chatHistoryCache.value[`agent_${idx}`] || []
addLog(t('log.selectChatTarget', { name: agent.username }))
addLog(t('log.selectChatTarget', { name: getAgentDisplayName(agent) }))
}
const formatTime = (timestamp) => {
@ -846,7 +867,7 @@ const sendToAgent = async (message) => {
throw new Error(t('step5.selectAgentFirst'))
}
addLog(t('log.sendToAgent', { name: selectedAgent.value.username, message: message.substring(0, 50) }))
addLog(t('log.sendToAgent', { name: getAgentDisplayName(selectedAgent.value), message: message.substring(0, 50) }))
// Build prompt with chat history
let prompt = message
@ -896,7 +917,7 @@ const sendToAgent = async (message) => {
content: responseContent,
timestamp: new Date().toISOString()
})
addLog(t('log.agentReplied', { name: selectedAgent.value.username }))
addLog(t('log.agentReplied', { name: getAgentDisplayName(selectedAgent.value) }))
} else {
throw new Error(t('step5.noResponse'))
}
@ -984,8 +1005,8 @@ const submitSurvey = async () => {
surveyResultsList.push({
agent_id: agentIdx,
agent_name: agent?.username || `Agent ${agentIdx}`,
profession: agent?.profession,
agent_name: getAgentDisplayName(agent, `Agent ${agentIdx}`),
profession: getAgentRoleLine(agent),
question: surveyQuestion.value.trim(),
answer: responseContent
})
@ -1987,7 +2008,8 @@ watch(() => props.simulationId, (newId) => {
top: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
min-width: 240px;
min-width: 360px;
max-width: min(460px, calc(100vw - 32px));
background: #FFFFFF;
border: 1px solid #E5E7EB;
border-radius: 12px;
@ -2072,6 +2094,14 @@ watch(() => props.simulationId, (newId) => {
text-overflow: ellipsis;
}
.agent-code {
font-size: 10px;
color: #6B7280;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Chat Messages */
.chat-messages {
flex: 1;