openvibers/openvibe.actor Commits

Add owner scoped agent sheet API (#424)

OP OpenVibers committed 2 days ago · e891c7a8e9 · 1 file, +38 −1

* Add owner scoped agent sheet API

* Keep agent sheet on declared API route

Changes

ddiff --git a/server/http/api.js b/server/http/api.js
iindex e5fa5be..bb04b51 100644
---- a/server/http/api.js
++++ b/server/http/api.js
@@@ -393,8 +393,45 @@ function createApi(ctx) {
res.set('Location', `/api/v1/me/agents/${out.row.id}`);
res.status(201).json(out.row);
});
++ // One owner-scoped read for the agent sheet. Keep each section sourced from Actor's existing records.
++ const agentSheet = async (req, res, agent) => {
++ const [tasks, asks, schedules, memory, timelineData] = await Promise.all([
++ store.listTasks(s, agent.owner, { limit: 100, via: `agent:${agent.id}` }),
++ s.db.many("SELECT id, kind, summary, payload, state, created_at, expires_at FROM actor_approvals WHERE owner = $1 AND agent_id = $2 ORDER BY (state = 'pending') DESC, created_at DESC LIMIT 50", [agent.owner, agent.id]),
++ s.db.many('SELECT id, task, kind, schedule, state, last_run_at, last_state FROM actor_triggers WHERE owner = $1 AND agent_id = $2 AND deleted_at IS NULL ORDER BY created_at DESC', [agent.owner, agent.id]),
++ personal.memory(s, agent.owner, agent.id),
++ timeline.list(s, agent.owner, { limit: 100 }),
++ ]);
++ const runs = await Promise.all(tasks.rows.map(async (task) => ({ id: task.id, task: task.task, state: task.state,
++ created_at: task.created_at, finished_at: task.finished_at, result: task.result || null, cost_usd: Number(task.cost_usd) || 0,
++ computer: await store.taskComputer(s, task) })));
++ const computerRun = runs.find((run) => run.computer);
++ const approvalsForAgent = asks.map((ask) => {
++ const payload = typeof ask.payload === 'string' ? JSON.parse(ask.payload) : ask.payload;
++ return { id: ask.id, kind: ask.kind, state: ask.state, what: ask.summary,
++ why: payload && payload.why || null, touch: payload && (payload.target || payload.resource) || null,
++ created_at: ask.created_at, expires_at: ask.expires_at };
++ });
++ res.set('Cache-Control', 'no-store').json({
++ agent: { id: agent.id, name: agent.name, face: agent.face || null,
++ state: agent.status === 'paused' ? 'paused' : (runs[0] && ['running', 'verifying'].includes(runs[0].state) ? 'working' : 'idle'),
++ instructions: agent.instructions, voice: agent.voice || null },
++ now: runs.find((run) => ['running', 'verifying'].includes(run.state)) || null,
++ work: runs, asks: approvalsForAgent,
++ asks_empty: approvalsForAgent.some((ask) => ask.state === 'pending') ? null : 'Requests to approve or decline will appear here.',
++ computer: computerRun ? { ...computerRun.computer, open: computerRun.computer.url || null } : null,
++ schedules: [{ schedule: agent.schedule, schedule_at: agent.schedule_at || null,
++ schedule_dow: agent.schedule_dow == null ? null : agent.schedule_dow,
++ next_run_at: agent.next_run_at || null, last_run_at: agent.last_run_at || null,
++ last_result: agent.last_skip_reason || runs.find((run) => run.finished_at)?.state || null },
++ ...schedules.map((item) => ({ ...item, next_run_at: null, last_result: item.last_state || null }))],
++ memory, activity: timelineData.entries.filter((entry) => tasks.rows.some((task) => entry.ref === task.id)),
++ });
++ };
r.get('/me/agents/:id', principal.requireCapability('actor.agent.manage'), owner, limits.reads('actor.agent.manage'), async (req, res) => {
-- const row = await loadAgent(req, res); if (row) res.json(row);
++ const row = await loadAgent(req, res); if (!row) return;
++ if (req.query.view === 'sheet') return agentSheet(req, res, row);
++ res.json(row);
});
// The agent's voice as a file (actor.voice@1). Another person's agent is the same 404 as one that does not exist.
r.get('/me/agents/:id/voice', principal.requireCapability('actor.agent.manage'), owner, limits.reads('actor.agent.manage'), async (req, res) => {