mirror of
https://github.com/theodore-song/polymarket-analyst.git
synced 2026-08-12 23:28:05 +00:00
Fold agent chat into live API
This commit is contained in:
@@ -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." });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+161
@@ -245,6 +245,163 @@ function validateIntent(intent) {
|
|||||||
const VALID_TICKET_STATUSES = new Set(["staged", "reviewed", "placed_manually", "skipped", "cancelled"]);
|
const VALID_TICKET_STATUSES = new Set(["staged", "reviewed", "placed_manually", "skipped", "cancelled"]);
|
||||||
const VALID_FILL_ACTIONS = new Set(["BUY", "SELL"]);
|
const VALID_FILL_ACTIONS = new Set(["BUY", "SELL"]);
|
||||||
const VALID_CAPITAL_ACTIONS = new Set(["DEPOSIT", "WITHDRAW", "BUY_AGENT", "SELL_AGENT"]);
|
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) {
|
function marketSearchUrl(question) {
|
||||||
return `https://polymarket.com/search?query=${encodeURIComponent(String(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") {
|
if (req.method === "POST") {
|
||||||
const body = typeof req.body === "string" ? JSON.parse(req.body || "{}") : (req.body || {});
|
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") {
|
if (body.action === "ticket") {
|
||||||
const ticket = normalizeTicket(body);
|
const ticket = normalizeTicket(body);
|
||||||
const ticketErrors = validateTicket(ticket);
|
const ticketErrors = validateTicket(ticket);
|
||||||
|
|||||||
+2
-2
@@ -2268,10 +2268,10 @@ function agentChatPayload(agentId,question,history=[]){
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
async function askAgentAI(agentId,question,history=[]){
|
async function askAgentAI(agentId,question,history=[]){
|
||||||
const r=await fetch("/api/agent-chat",{
|
const r=await fetch("/api/live",{
|
||||||
method:"POST",
|
method:"POST",
|
||||||
headers:{"Content-Type":"application/json"},
|
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(()=>({}));
|
const data=await r.json().catch(()=>({}));
|
||||||
if(!r.ok||!data.ok)throw new Error(data.error||"Agent AI chat is not available yet.");
|
if(!r.ok||!data.ok)throw new Error(data.error||"Agent AI chat is not available yet.");
|
||||||
|
|||||||
Reference in New Issue
Block a user