203 lines
8.0 KiB
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()]);
|
|
}
|