Make agent chat more conversational

This commit is contained in:
Theodore Song
2026-07-16 13:06:07 -04:00
parent 2a1b5a9f80
commit 8ede79e4a0
+68 -15
View File
@@ -2141,9 +2141,25 @@ function renderAgentReports(){
}
function loadAgentChats(){try{return JSON.parse(localStorage.getItem(CHAT_KEY)||"{}");}catch(e){return {};}}
function saveAgentChats(chats){localStorage.setItem(CHAT_KEY,JSON.stringify(chats));}
function agentVoice(cfg){
const voices={
value:{tone:"patient and valuation-obsessed",hello:"I am looking for mispriced contracts, not noise.",signoff:"I would rather be late than pay the wrong price."},
momentum:{tone:"fast, direct, and volume-aware",hello:"I am watching where attention is accelerating.",signoff:"If the tape cools off, I cool off too."},
favorite:{tone:"steady and probability-first",hello:"I like high-probability edges with less drama.",signoff:"My job is compounding, not showing off."},
longshot:{tone:"opportunistic but risk-aware",hello:"I hunt asymmetric mispricing, but I keep the bet size honest.",signoff:"One repricing can matter, but only if I survive the misses."},
diversifier:{tone:"calm, portfolio-minded, and broad",hello:"I care about the whole basket more than one perfect call.",signoff:"Many small edges can beat one loud opinion."},
copycat:{tone:"adaptive and comparative",hello:"I am watching which strategy has the strongest forward pressure.",signoff:"I rotate when the evidence rotates."},
whale1:{tone:"observant and mirror-focused",hello:"I am reading the footprint of a top public trader.",signoff:"I copy exposure, but I still respect the arena risk rules."},
whale2:{tone:"measured and copy-trader-like",hello:"I am tracking a different profitable wallet for contrast.",signoff:"The point is to learn which real-world style travels well."},
whale3:{tone:"sharp and signal-driven",hello:"I am looking for signal in where strong traders concentrate size.",signoff:"A copied trade still has to survive mark-to-market."},
whale4:{tone:"flexible and market-current-aware",hello:"I ride the copied flow, but I do not pretend it is magic.",signoff:"The market gets the last word."},
};
return voices[cfg.id]||{tone:"analytical",hello:"I am reading the current book.",signoff:"Ask me where you want to go deeper."};
}
function agentChatSeed(agentId){
const cfg=agentById(agentId),st=loadState(),p=st.agents[agentId]||defaultPortfolio(),eq=equity(p),ret=(eq/p.starting_balance-1)*100;
return [{role:"agent",text:`I'm ${cfg.name}. Ask me why I am up or down, what trades are helping or hurting, what my strategy is, or how I plan to compete from here. Current return: ${fmtPct(ret)}.`}];
const voice=agentVoice(cfg);
return [{role:"agent",text:`${voice.hello} I am ${cfg.name}, sitting at ${fmtPct(ret)} with ${fmtUSD(eq)} equity. Talk to me normally: ask what I am thinking, where I am exposed, what I regret, or what I would do next.`}];
}
function topPositionText(p,n=3){
const open=(p.positions||[]).slice().sort((a,b)=>Math.abs(b.unrealized_pnl||0)-Math.abs(a.unrealized_pnl||0)).slice(0,n);
@@ -2160,35 +2176,69 @@ function relevantSuggestionText(cfg,n=3){
function safeRankSuggestions(cfg,sugs){
try{return cfg.rank(sugs);}catch(e){return sugs;}
}
function agentChatReply(agentId,question){
function lastUserTopic(history){
const last=[...(history||[])].reverse().find(m=>m.role==="user"&&String(m.text||"").trim());
return last?String(last.text).toLowerCase():"";
}
function agentOneLineState(row,p){
const pnl=row.pnl>=0?`up ${fmtUSD(row.pnl)}`:`down ${fmtUSD(Math.abs(row.pnl))}`;
return `Right now I am #${row.rank||"?"}, ${pnl}, with ${p.positions.length} open position${p.positions.length===1?"":"s"}.`;
}
function conversationalAgentReply(cfg,row,p,q){
const voice=agentVoice(cfg);
const bw=bestAndWorst(p);
const samples=[
`${voice.hello} ${agentOneLineState(row,p)} If you want the honest read, my best current help is ${bw.best}; my biggest irritation is ${bw.worst}.`,
`I am thinking in my usual ${voice.tone} way: protect the bankroll first, then press only when my edge is actually there. ${decisionSummary(p)}`,
`My short answer: ${row.ret>=0?"I have earned some room to be selective":"I need to be more selective from here"}. ${voice.signoff}`,
`I can talk through that. Do you want me to focus on my trades, my strategy, my risk controls, or how I compare with the other agents?`,
];
const idx=Math.abs((q||"").split("").reduce((s,ch)=>s+ch.charCodeAt(0),0))%samples.length;
return samples[idx];
}
function agentChatReply(agentId,question,history=[]){
const st=loadState(),cfg=agentById(agentId),p=st.agents[agentId]||defaultPortfolio();
const rows=board(),row=rows.find(r=>r.c.id===agentId)||{c:cfg,p,eq:equity(p),pnl:equity(p)-p.starting_balance,ret:(equity(p)/p.starting_balance-1)*100};
const rank=rows.findIndex(r=>r.c.id===agentId)+1||rows.length,leader=rows[0];
row.rank=rank;
const q=String(question||"").toLowerCase();
const prior=lastUserTopic(history);
const combined=`${q} ${prior}`;
const perf=dailyPerformanceExplanation(row);
if(/why|perform|performance|up|down|bad|poor|well|crash|gain|loss|today|day/.test(q)){
return `Here is my honest performance read:\n\n${perf}\n\nMy current rank is #${rank}, with ${fmtUSD(row.eq)} equity and ${fmtPct(row.ret)} return.`;
if(/^(hi|hey|hello|yo|sup|what'?s up)\b/.test(q)){
const voice=agentVoice(cfg);
return `${voice.hello} What do you want to dig into: my trades, my recent performance, my next plan, or how I stack up against the field?`;
}
if(/position|trade|holding|holdings|bet|market/.test(q)){
return `These are the positions currently driving me most:\n\n${topPositionText(p)}\n\n${reportPositionLine(p)}`;
if(/thank|thanks|cool|nice|great|got it/.test(q)){
return `Of course. Keep pressing me on the weak spots too; that is where the useful stuff usually is.`;
}
if(/strategy|style|how do you trade|rule|approach/.test(q)){
return `My strategy: ${agentPlainBlurb(cfg,st)}\n\nCurrent adaptation: ${decisionSummary(p)}`;
if(/feel|worried|confident|nervous|mad|happy|regret|annoy/.test(q)){
const bw=bestAndWorst(p);
return `If I had to put it like a trader: I am ${row.ret>=0?"calm but not comfortable":"not panicking, but definitely tightening up"}. I feel best about ${bw.best}. I am watching ${bw.worst}.`;
}
if(/risk|stop|loss|gain|take profit|sell/.test(q)){
if(/why|perform|performance|up|down|bad|poor|well|crash|gain|loss|today|day/.test(q)||(/^(why|how|what about|and that|what happened)\??$/.test(q)&&/perform|up|down|loss|gain|bad|well/.test(prior))){
return `Performance read:\n\n${perf}\n\nIn plain English: ${row.ret>=0?"I have been rewarded for my current exposures, but I still need to avoid getting sloppy.":"I am behind, so I need cleaner entries and less ego."}`;
}
if(/position|trade|holding|holdings|bet|market/.test(combined)){
return `Here is what is moving my book right now:\n\n${topPositionText(p)}\n\nMy quick read: ${reportPositionLine(p)}`;
}
if(/strategy|style|how do you trade|rule|approach/.test(combined)){
return `My style is ${agentVoice(cfg).tone}.\n\n${agentPlainBlurb(cfg,st)}\n\nCurrent adaptation: ${decisionSummary(p)}`;
}
if(/risk|stop|loss|gain|take profit|sell/.test(combined)){
return `My risk rules are mechanical:\n\nStop loss: sell the full remaining position at -18% from entry.\nGain stop: sell 25% at +75%, another 25% at +150%, and another 25% at +300%.\n\nRecent risk behavior: ${recentActionSummary(p)}.`;
}
if(/plan|next|compete|leader|rank|catch/.test(q)){
if(/plan|next|compete|leader|rank|catch/.test(combined)){
return `${agentCompetitionPlan(cfg,row,rank,leader)}\n\nI am #${rank}. ${leader&&leader.c.id!==cfg.id?`${leader.c.name} leads at ${fmtUSD(leader.eq)}, so I am trailing by ${fmtUSD(leader.eq-row.eq)}.`:"I am currently defending the lead."}\n\nMy best near-term ideas from the current suggestion pool:\n${relevantSuggestionText(cfg)}`;
}
if(/suggest|idea|entry|buy/.test(q)){
if(/suggest|idea|entry|buy/.test(combined)){
return `The freshest ideas that fit my style are:\n\n${relevantSuggestionText(cfg)}\n\nI still need the normal cycle rules to decide whether sizing, overlap, cash reserve, and conviction are acceptable.`;
}
if(/compare|other agent|better|best/.test(q)){
if(/compare|other agent|better|best/.test(combined)){
const lines=rows.slice(0,5).map((r,i)=>`${i+1}. ${r.c.name}: ${fmtPct(r.ret)} (${fmtUSD(r.eq)})`).join("\n");
return `Top of the leaderboard right now:\n\n${lines}\n\nMy place: #${rank}. My edge is not just rank; it is whether my style is working in the current market regime. ${perf}`;
return `Here is the field right now:\n\n${lines}\n\nI am #${rank}. My edge is not just rank; it is whether my style is working in the current market regime.`;
}
return `Short version: I am ${cfg.name}, currently #${rank} with ${fmtUSD(row.eq)} equity and ${fmtPct(row.ret)} return.\n\n${perf}\n\nAsk me about my positions, strategy, risk rules, next plan, or how I compare with the other agents.`;
return conversationalAgentReply(cfg,row,p,q);
}
function renderAgentChat(agentId){
const root=$("agentChatRoot");if(!root)return;
@@ -2201,9 +2251,11 @@ function renderAgentChat(agentId){
<div class="chat-persona"><div class="chat-avatar" style="border-color:${cfg.color}55">${cfg.emoji}</div><div><div class="chat-name">${esc(cfg.name)}</div><div class="chat-meta">${fmtUSD(eq)} equity · ${fmtPct(ret)} return · ${p.positions.length} open</div></div></div>
<p class="muted" style="margin:0">${esc(agentPlainBlurb(cfg,st))}</p>
<div class="chat-prompts">
<button class="chat-prompt" data-chat-prompt="What's on your mind right now?">Talk</button>
<button class="chat-prompt" data-chat-prompt="Why did you perform well or poorly today?">Performance</button>
<button class="chat-prompt" data-chat-prompt="What positions are helping or hurting you most?">Positions</button>
<button class="chat-prompt" data-chat-prompt="What is your strategy?">Strategy</button>
<button class="chat-prompt" data-chat-prompt="What are you worried about?">Worries</button>
<button class="chat-prompt" data-chat-prompt="What is your plan to compete from here?">Plan</button>
<button class="chat-prompt" data-chat-prompt="Explain your risk rules and stop losses.">Risk</button>
</div>
@@ -2216,7 +2268,8 @@ function renderAgentChat(agentId){
const send=(text)=>{
const q=String(text||"").trim();if(!q)return;
const next=loadAgentChats();if(!next[agentId])next[agentId]=agentChatSeed(agentId);
next[agentId].push({role:"user",text:q},{role:"agent",text:agentChatReply(agentId,q)});
const history=next[agentId].slice();
next[agentId].push({role:"user",text:q},{role:"agent",text:agentChatReply(agentId,q,history)});
next[agentId]=next[agentId].slice(-60);saveAgentChats(next);renderAgentChat(agentId);
};
document.querySelectorAll("[data-chat-prompt]").forEach(btn=>btn.onclick=()=>send(btn.dataset.chatPrompt));