From 14d68062dad3e711e1bf04d8d3a4090f906b5e1b Mon Sep 17 00:00:00 2001 From: Theodore Song Date: Thu, 16 Jul 2026 22:11:59 -0400 Subject: [PATCH] Fold agent chat into live API --- api/agent-chat.js | 168 ---------------------------------------------- api/live.js | 161 ++++++++++++++++++++++++++++++++++++++++++++ index.html | 4 +- 3 files changed, 163 insertions(+), 170 deletions(-) delete mode 100644 api/agent-chat.js diff --git a/api/agent-chat.js b/api/agent-chat.js deleted file mode 100644 index 6dfbfd9..0000000 --- a/api/agent-chat.js +++ /dev/null @@ -1,168 +0,0 @@ -const MAX_HISTORY = 12; -const MAX_POSITIONS = 10; -const MAX_SUGGESTIONS = 8; - -function cleanText(value, max = 1200) { - return String(value || "").replace(/\s+/g, " ").trim().slice(0, max); -} - -function safeNumber(value, fallback = 0) { - const n = Number(value); - return Number.isFinite(n) ? n : fallback; -} - -function compactPosition(pos) { - return { - question: cleanText(pos.question, 180), - side: cleanText(pos.side, 8), - entry_price: safeNumber(pos.entry_price), - current_price: safeNumber(pos.current_price), - value: safeNumber(pos.value), - unrealized_pnl: safeNumber(pos.unrealized_pnl), - conviction: safeNumber(pos.conviction), - category: cleanText(pos.category, 60), - opened_at: cleanText(pos.opened_at, 40), - url: cleanText(pos.url, 240), - }; -} - -function compactSuggestion(sug) { - return { - question: cleanText(sug.question, 180), - side: cleanText(sug.side, 8), - entry_price: safeNumber(sug.entry_price), - conviction: safeNumber(sug.conviction), - edge: safeNumber(sug.edge), - category: cleanText(sug.category, 60), - rationale: cleanText(sug.rationale, 240), - url: cleanText(sug.url, 240), - }; -} - -function compactHistory(history) { - return (Array.isArray(history) ? history : []).slice(-MAX_HISTORY).map((m) => ({ - role: m && m.role === "user" ? "user" : "assistant", - text: cleanText(m && m.text, 900), - })).filter((m) => m.text); -} - -function outputText(data) { - if (data && typeof data.output_text === "string") return data.output_text.trim(); - const chunks = []; - for (const item of data && Array.isArray(data.output) ? data.output : []) { - for (const content of Array.isArray(item.content) ? item.content : []) { - if (content.type === "output_text" && content.text) chunks.push(content.text); - if (content.type === "text" && content.text) chunks.push(content.text); - } - } - return chunks.join("\n").trim(); -} - -export default async function handler(req, res) { - res.setHeader("Cache-Control", "no-store"); - if (req.method !== "POST") { - res.setHeader("Allow", "POST"); - return res.status(405).json({ ok: false, error: "Method not allowed" }); - } - - const apiKey = process.env.OPENAI_API_KEY; - if (!apiKey) { - return res.status(501).json({ - ok: false, - code: "missing_openai_key", - error: "Agent AI chat needs OPENAI_API_KEY in Vercel environment variables.", - }); - } - - const body = req.body || {}; - const question = cleanText(body.question, 1000); - if (!question) return res.status(400).json({ ok: false, error: "Question is required." }); - - const agent = body.agent || {}; - const portfolio = body.portfolio || {}; - const context = { - agent: { - id: cleanText(agent.id, 40), - name: cleanText(agent.name, 80), - kind: cleanText(agent.kind, 40), - style: cleanText(agent.style || agent.blurb, 800), - voice: cleanText(agent.voice, 200), - }, - portfolio: { - equity: safeNumber(portfolio.equity), - cash: safeNumber(portfolio.cash), - return_pct: safeNumber(portfolio.return_pct), - pnl: safeNumber(portfolio.pnl), - rank: safeNumber(portfolio.rank), - open_positions: safeNumber(portfolio.open_positions), - last_decision: cleanText(portfolio.last_decision, 700), - recent_actions: (Array.isArray(portfolio.recent_actions) ? portfolio.recent_actions : []).slice(-8).map((x) => cleanText(x, 260)), - positions: (Array.isArray(portfolio.positions) ? portfolio.positions : []).slice(0, MAX_POSITIONS).map(compactPosition), - snapshots: (Array.isArray(portfolio.snapshots) ? portfolio.snapshots : []).slice(-8).map((s) => ({ - date: cleanText(s.date || s.timestamp, 40), - equity: safeNumber(s.equity), - return_pct: safeNumber(s.return_pct), - open_positions: safeNumber(s.open_positions), - })), - }, - leaderboard: (Array.isArray(body.leaderboard) ? body.leaderboard : []).slice(0, 10).map((r) => ({ - name: cleanText(r.name, 80), - return_pct: safeNumber(r.return_pct), - equity: safeNumber(r.equity), - rank: safeNumber(r.rank), - })), - suggestions: (Array.isArray(body.suggestions) ? body.suggestions : []).slice(0, MAX_SUGGESTIONS).map(compactSuggestion), - history: compactHistory(body.history), - }; - - const instructions = [ - `You are ${context.agent.name || "a Polymarket paper-trading agent"} inside Poly Arena.`, - "Speak in first person as the selected agent, like a real chat partner.", - "Answer the user's exact question instead of repeating a generic performance summary.", - "Use the supplied portfolio, positions, leaderboard, suggestions, and chat history as your facts.", - "Be specific about trades, risk, performance, and uncertainty when the data is available.", - "Do not claim you can guarantee profits or force agents to make money.", - "Do not give personalized financial advice. Keep it framed as paper trading, research, or manual review.", - "If asked what you will do next, describe likely decision rules, not a guaranteed action.", - "Keep responses concise: 2 to 5 short paragraphs or a tight bullet list.", - ].join("\n"); - - const input = [ - { - role: "user", - content: [{ - type: "input_text", - text: `Context JSON:\n${JSON.stringify(context)}\n\nUser message:\n${question}`, - }], - }, - ]; - - try { - const response = await fetch("https://api.openai.com/v1/responses", { - method: "POST", - headers: { - "Authorization": `Bearer ${apiKey}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - model: process.env.OPENAI_MODEL || "gpt-5.6-luna", - instructions, - input, - max_output_tokens: 550, - }), - }); - - const data = await response.json().catch(() => ({})); - if (!response.ok) { - return res.status(response.status).json({ - ok: false, - error: data && data.error && data.error.message ? data.error.message : "OpenAI chat request failed.", - }); - } - - const text = outputText(data); - return res.status(200).json({ ok: true, text: text || "I am here, but I could not form a useful answer from the current context." }); - } catch (err) { - return res.status(500).json({ ok: false, error: err && err.message ? err.message : "Agent chat failed." }); - } -} diff --git a/api/live.js b/api/live.js index b9bdb29..02c77d8 100644 --- a/api/live.js +++ b/api/live.js @@ -245,6 +245,163 @@ function validateIntent(intent) { const VALID_TICKET_STATUSES = new Set(["staged", "reviewed", "placed_manually", "skipped", "cancelled"]); const VALID_FILL_ACTIONS = new Set(["BUY", "SELL"]); const VALID_CAPITAL_ACTIONS = new Set(["DEPOSIT", "WITHDRAW", "BUY_AGENT", "SELL_AGENT"]); +const AGENT_CHAT_MAX_HISTORY = 12; +const AGENT_CHAT_MAX_POSITIONS = 10; +const AGENT_CHAT_MAX_SUGGESTIONS = 8; + +function cleanAgentText(value, max = 1200) { + return String(value || "").replace(/\s+/g, " ").trim().slice(0, max); +} + +function safeAgentNumber(value, fallback = 0) { + const n = Number(value); + return Number.isFinite(n) ? n : fallback; +} + +function compactAgentPosition(pos) { + return { + question: cleanAgentText(pos.question, 180), + side: cleanAgentText(pos.side, 8), + entry_price: safeAgentNumber(pos.entry_price), + current_price: safeAgentNumber(pos.current_price), + value: safeAgentNumber(pos.value), + unrealized_pnl: safeAgentNumber(pos.unrealized_pnl), + conviction: safeAgentNumber(pos.conviction), + category: cleanAgentText(pos.category, 60), + opened_at: cleanAgentText(pos.opened_at, 40), + url: cleanAgentText(pos.url, 240), + }; +} + +function compactAgentSuggestion(sug) { + return { + question: cleanAgentText(sug.question, 180), + side: cleanAgentText(sug.side, 8), + entry_price: safeAgentNumber(sug.entry_price), + conviction: safeAgentNumber(sug.conviction), + edge: safeAgentNumber(sug.edge), + category: cleanAgentText(sug.category, 60), + rationale: cleanAgentText(sug.rationale, 240), + url: cleanAgentText(sug.url, 240), + }; +} + +function compactAgentHistory(history) { + return (Array.isArray(history) ? history : []).slice(-AGENT_CHAT_MAX_HISTORY).map((m) => ({ + role: m && m.role === "user" ? "user" : "assistant", + text: cleanAgentText(m && m.text, 900), + })).filter((m) => m.text); +} + +function openAIOutputText(data) { + if (data && typeof data.output_text === "string") return data.output_text.trim(); + const chunks = []; + for (const item of data && Array.isArray(data.output) ? data.output : []) { + for (const content of Array.isArray(item.content) ? item.content : []) { + if (content.type === "output_text" && content.text) chunks.push(content.text); + if (content.type === "text" && content.text) chunks.push(content.text); + } + } + return chunks.join("\n").trim(); +} + +async function handleAgentChat(body, res) { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + return res.status(501).json({ + ok: false, + code: "missing_openai_key", + error: "Agent AI chat needs OPENAI_API_KEY in Vercel environment variables.", + }); + } + + const question = cleanAgentText(body.question, 1000); + if (!question) return res.status(400).json({ ok: false, error: "Question is required." }); + + const agent = body.agent || {}; + const portfolio = body.portfolio || {}; + const context = { + agent: { + id: cleanAgentText(agent.id, 40), + name: cleanAgentText(agent.name, 80), + kind: cleanAgentText(agent.kind, 40), + style: cleanAgentText(agent.style || agent.blurb, 800), + voice: cleanAgentText(agent.voice, 200), + }, + portfolio: { + equity: safeAgentNumber(portfolio.equity), + cash: safeAgentNumber(portfolio.cash), + return_pct: safeAgentNumber(portfolio.return_pct), + pnl: safeAgentNumber(portfolio.pnl), + rank: safeAgentNumber(portfolio.rank), + open_positions: safeAgentNumber(portfolio.open_positions), + last_decision: cleanAgentText(portfolio.last_decision, 700), + recent_actions: (Array.isArray(portfolio.recent_actions) ? portfolio.recent_actions : []).slice(-8).map((x) => cleanAgentText(x, 260)), + positions: (Array.isArray(portfolio.positions) ? portfolio.positions : []).slice(0, AGENT_CHAT_MAX_POSITIONS).map(compactAgentPosition), + snapshots: (Array.isArray(portfolio.snapshots) ? portfolio.snapshots : []).slice(-8).map((s) => ({ + date: cleanAgentText(s.date || s.timestamp, 40), + equity: safeAgentNumber(s.equity), + return_pct: safeAgentNumber(s.return_pct), + open_positions: safeAgentNumber(s.open_positions), + })), + }, + leaderboard: (Array.isArray(body.leaderboard) ? body.leaderboard : []).slice(0, 10).map((r) => ({ + name: cleanAgentText(r.name, 80), + return_pct: safeAgentNumber(r.return_pct), + equity: safeAgentNumber(r.equity), + rank: safeAgentNumber(r.rank), + })), + suggestions: (Array.isArray(body.suggestions) ? body.suggestions : []).slice(0, AGENT_CHAT_MAX_SUGGESTIONS).map(compactAgentSuggestion), + history: compactAgentHistory(body.history), + }; + + const instructions = [ + `You are ${context.agent.name || "a Polymarket paper-trading agent"} inside Poly Arena.`, + "Speak in first person as the selected agent, like a real chat partner.", + "Answer the user's exact question instead of repeating a generic performance summary.", + "Use the supplied portfolio, positions, leaderboard, suggestions, and chat history as your facts.", + "Be specific about trades, risk, performance, and uncertainty when the data is available.", + "Do not claim you can guarantee profits or force agents to make money.", + "Do not give personalized financial advice. Keep it framed as paper trading, research, or manual review.", + "If asked what you will do next, describe likely decision rules, not a guaranteed action.", + "Keep responses concise: 2 to 5 short paragraphs or a tight bullet list.", + ].join("\n"); + + try { + const response = await fetch("https://api.openai.com/v1/responses", { + method: "POST", + headers: { + "Authorization": `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: process.env.OPENAI_MODEL || "gpt-5.6-luna", + instructions, + input: [{ + role: "user", + content: [{ + type: "input_text", + text: `Context JSON:\n${JSON.stringify(context)}\n\nUser message:\n${question}`, + }], + }], + max_output_tokens: 550, + }), + }); + + const data = await response.json().catch(() => ({})); + if (!response.ok) { + return res.status(response.status).json({ + ok: false, + error: data && data.error && data.error.message ? data.error.message : "OpenAI chat request failed.", + }); + } + + const text = openAIOutputText(data); + return res.status(200).json({ ok: true, text: text || "I am here, but I could not form a useful answer from the current context." }); + } catch (err) { + return res.status(500).json({ ok: false, error: err && err.message ? err.message : "Agent chat failed." }); + } +} function marketSearchUrl(question) { return `https://polymarket.com/search?query=${encodeURIComponent(String(question || ""))}`; @@ -366,6 +523,10 @@ export default async function handler(req, res) { if (req.method === "POST") { const body = typeof req.body === "string" ? JSON.parse(req.body || "{}") : (req.body || {}); + if (body.action === "agent_chat") { + return handleAgentChat(body, res); + } + if (body.action === "ticket") { const ticket = normalizeTicket(body); const ticketErrors = validateTicket(ticket); diff --git a/index.html b/index.html index b466f6e..0a14bff 100644 --- a/index.html +++ b/index.html @@ -2268,10 +2268,10 @@ function agentChatPayload(agentId,question,history=[]){ }; } async function askAgentAI(agentId,question,history=[]){ - const r=await fetch("/api/agent-chat",{ + const r=await fetch("/api/live",{ method:"POST", headers:{"Content-Type":"application/json"}, - body:JSON.stringify(agentChatPayload(agentId,question,history)), + body:JSON.stringify(Object.assign({action:"agent_chat"},agentChatPayload(agentId,question,history))), }); const data=await r.json().catch(()=>({})); if(!r.ok||!data.ok)throw new Error(data.error||"Agent AI chat is not available yet.");