Files
html/api/paperclip-bridge.php

203 lines
8.0 KiB
PHP

<?php
/**
* paperclip-bridge.php — Pont Meeting Rooms / Cloudbot Social → Paperclip AI
*
* Reçoit des actions concrètes issues de discussions IA et les transforme en
* tâches Paperclip EXÉCUTÉES (code, commit, deploy, etc.)
*
* POST JSON:
* {
* "source": "meeting-rooms|cloudbot-social",
* "action": "description courte",
* "prompt": "consigne détaillée pour Paperclip",
* "agents_discussed": ["WEVIA Master", "Ethica"],
* "priority": "low|normal|high",
* "session_id": "optional for follow-up"
* }
*
* GET ?action=list : liste les N dernières actions avec leur statut
* GET ?action=status&id=UUID : statut d'une action spécifique
*/
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
// DB Paperclip (local postgres)
$DSN = 'pgsql:host=127.0.0.1;port=5432;dbname=paperclip';
$DB_USER = 'paperclip';
$DB_PASS = 'PaperclipWeval2026';
// Company + Agent connus (WEVAL + claude_local agent)
$COMPANY_ID = 'dd12987b-c774-45e7-95fd-d34003f91650';
$AGENT_ID = 'b4eb33d3-7249-48db-9d67-778cf90854cd'; // claude_local heartbeat agent
try {
$pdo = new PDO($DSN, $DB_USER, $DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_TIMEOUT => 5,
]);
} catch (Throwable $e) {
http_response_code(503);
echo json_encode(['error' => 'Paperclip DB indisponible', 'detail' => $e->getMessage()]);
exit;
}
// === GET actions ===
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$action = $_GET['action'] ?? 'list';
if ($action === 'list') {
$limit = min(50, max(1, intval($_GET['limit'] ?? 10)));
$stmt = $pdo->prepare("
SELECT wr.id, wr.source, wr.reason, wr.status, wr.requested_at, wr.finished_at, wr.error,
wr.payload, a.name as agent_name
FROM agent_wakeup_requests wr
LEFT JOIN agents a ON a.id = wr.agent_id
WHERE wr.source LIKE 'weval-social:%' OR wr.source LIKE 'meeting-rooms:%' OR wr.source LIKE 'cloudbot-social:%'
ORDER BY wr.requested_at DESC
LIMIT :lim
");
$stmt->bindValue(':lim', $limit, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['ok' => true, 'count' => count($rows), 'actions' => $rows]);
exit;
}
if ($action === 'status' && !empty($_GET['id'])) {
$id = $_GET['id'];
$stmt = $pdo->prepare("
SELECT wr.*, hr.status as run_status, hr.started_at, hr.finished_at as run_finished, hr.result_json, hr.exit_code
FROM agent_wakeup_requests wr
LEFT JOIN heartbeat_runs hr ON hr.wakeup_request_id = wr.id
WHERE wr.id = :id
LIMIT 1
");
$stmt->bindValue(':id', $id);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
echo json_encode(['ok' => true, 'action' => $row]);
} else {
echo json_encode(['ok' => false, 'error' => 'not found']);
}
exit;
}
if ($action === 'stats') {
$stmt = $pdo->query("
SELECT
COUNT(*) FILTER (WHERE source LIKE 'weval-social:%' OR source LIKE 'meeting-rooms:%' OR source LIKE 'cloudbot-social:%') as total,
COUNT(*) FILTER (WHERE (source LIKE 'weval-social:%' OR source LIKE 'meeting-rooms:%' OR source LIKE 'cloudbot-social:%') AND status = 'completed') as completed,
COUNT(*) FILTER (WHERE (source LIKE 'weval-social:%' OR source LIKE 'meeting-rooms:%' OR source LIKE 'cloudbot-social:%') AND status = 'queued') as queued,
COUNT(*) FILTER (WHERE (source LIKE 'weval-social:%' OR source LIKE 'meeting-rooms:%' OR source LIKE 'cloudbot-social:%') AND status = 'running') as running,
COUNT(*) FILTER (WHERE (source LIKE 'weval-social:%' OR source LIKE 'meeting-rooms:%' OR source LIKE 'cloudbot-social:%') AND status = 'failed') as failed
FROM agent_wakeup_requests
");
$s = $stmt->fetch(PDO::FETCH_ASSOC);
echo json_encode(['ok' => true, 'stats' => $s]);
exit;
}
echo json_encode(['ok' => false, 'error' => 'unknown action', 'hint' => 'use ?action=list|status|stats']);
exit;
}
// === POST : créer une nouvelle tâche Paperclip ===
$raw = file_get_contents('php://input');
$input = json_decode($raw, true);
if (!$input || empty($input['action']) || empty($input['prompt'])) {
http_response_code(400);
echo json_encode([
'error' => 'payload invalide',
'required' => ['action', 'prompt'],
'optional' => ['source', 'agents_discussed', 'priority', 'session_id']
]);
exit;
}
$source = $input['source'] ?? 'cloudbot-social';
if (!in_array($source, ['meeting-rooms', 'cloudbot-social', 'weval-social'])) {
$source = 'cloudbot-social';
}
$action = substr($input['action'], 0, 200);
$prompt = $input['prompt'];
$agents = $input['agents_discussed'] ?? [];
$priority = $input['priority'] ?? 'normal';
$session_id = $input['session_id'] ?? null;
// Build payload JSON qui sera lu par Paperclip
$payload = [
'type' => 'weval-social-action',
'source' => $source,
'action' => $action,
'prompt' => $prompt,
'agents_discussed' => $agents,
'priority' => $priority,
'requested_by' => 'weval-social-bridge',
'requested_at' => date('c'),
'meta' => [
'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
'ua' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 120),
'session' => $session_id,
]
];
$idempotency = hash('sha256', $source . '|' . $action . '|' . substr($prompt, 0, 500) . '|' . floor(time() / 60));
// Insert wakeup request
try {
$stmt = $pdo->prepare("
INSERT INTO agent_wakeup_requests (company_id, agent_id, source, reason, trigger_detail, payload, status, requested_by_actor_type, requested_by_actor_id, idempotency_key)
VALUES (:cid, :aid, :src, :reason, :trig, :pl::jsonb, 'queued', 'system', 'weval-social-bridge', :ikey)
RETURNING id, requested_at
");
$stmt->bindValue(':cid', $COMPANY_ID);
$stmt->bindValue(':aid', $AGENT_ID);
$stmt->bindValue(':src', $source . ':' . substr($action, 0, 30));
$stmt->bindValue(':reason', substr($action . ' — ' . $prompt, 0, 500));
$stmt->bindValue(':trig', 'weval-social:' . $action);
$stmt->bindValue(':pl', json_encode($payload));
$stmt->bindValue(':ikey', $idempotency);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);
// Log pour traçabilité side-channel
$logDir = '/opt/weval-ops/paperclip-bridge-logs';
if (!is_dir($logDir)) @mkdir($logDir, 0755, true);
@file_put_contents(
$logDir . '/' . date('Y-m-d') . '.jsonl',
json_encode(['id' => $row['id'], 'requested_at' => $row['requested_at']] + $payload) . "\n",
FILE_APPEND
);
echo json_encode([
'ok' => true,
'id' => $row['id'],
'status' => 'queued',
'source' => $source,
'action' => $action,
'agents_discussed' => $agents,
'requested_at' => $row['requested_at'],
'info' => 'Paperclip va traiter cette action. Utilisez GET ?action=status&id=' . $row['id'] . ' pour suivre.',
'status_url' => '/api/paperclip-bridge.php?action=status&id=' . $row['id']
]);
} catch (Throwable $e) {
// Si idempotency violation, retourner OK avec l'ancienne
if (strpos($e->getMessage(), 'idempotency') !== false || strpos($e->getMessage(), 'unique') !== false) {
$stmt = $pdo->prepare("SELECT id, status, requested_at FROM agent_wakeup_requests WHERE idempotency_key = :k LIMIT 1");
$stmt->bindValue(':k', $idempotency);
$stmt->execute();
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
if ($existing) {
echo json_encode(['ok' => true, 'existing' => true] + $existing);
exit;
}
}
http_response_code(500);
echo json_encode(['error' => 'insert failed', 'detail' => $e->getMessage()]);
}