agent-sync/register_agents.py
2025-10-21 07:36:11 -05:00

299 lines
12 KiB
Python
Executable file

#!/usr/bin/env python3
"""
Register agents from a JSON file with the Agent Registration API.
Usage:
python register_agents.py --input /path/to/shared_agents.json \
[--base-url https://ai-sandbox.oliver.solutions/agent_collector/agents] \
[--api-key YOUR_KEY] \
[--dry-run]
Notes:
- If --api-key is not provided, the script will fall back to the static key from the docs.
- The input can be either:
* a list of agent documents, or
* a list of wrapper objects that contain an "agentDetails" object (common when exported from MongoDB aggregations).
- The script maps fields best-effort to the API schema and prunes any empty/None fields.
"""
import argparse
import json
import os
import re
import sys
import time
import urllib3
from typing import Any, Dict, List, Optional
# Suppress SSL warnings when verification is disabled
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
try:
import requests # type: ignore
except Exception as e:
print("This script requires the 'requests' package. Install with: pip install requests")
raise
DEFAULT_BASE_URL = "https://ai-sandbox.oliver.solutions/agent_collector/agents"
# Fallback to the static key from the documentation if not supplied via --api-key
DEFAULT_API_KEY = "agent-collector-static-key-2024-secure"
def parse_iso(value: Any) -> Optional[str]:
"""
Best-effort: return ISO8601 string or None.
- If value is already a string, return it (assuming it's already ISO8601).
- If value looks like 'ISODate(\"...\")', extract the inner string.
- Otherwise, return None.
"""
if value is None:
return None
if isinstance(value, str):
# Handle Mongo shell export style: ISODate('2025-07-09T06:33:12.682Z')
m = re.match(r"^ISODate\(['\"]?([^'\"]+)['\"]?\)$", value.strip())
return m.group(1) if m else value
# Could try to parse datetime objects here if needed; for now, prefer string passthrough
return None
def ensure_list_of_str(value: Any) -> Optional[List[str]]:
"""Convert to a list[str] if reasonable; otherwise return None."""
if value is None:
return None
if isinstance(value, list):
out = []
for v in value:
if isinstance(v, str):
out.append(v)
else:
out.append(str(v))
return out if out else None
# Single string -> wrap
if isinstance(value, str) and value.strip():
return [value]
return None
def extract_agent(doc: Dict[str, Any]) -> Dict[str, Any]:
"""Handle two shapes: raw agent doc OR wrapper { ..., agentDetails: {...} }"""
if isinstance(doc, dict) and "agentDetails" in doc and isinstance(doc["agentDetails"], dict):
return doc["agentDetails"]
return doc
def build_payload(agent: Dict[str, Any]) -> Dict[str, Any]:
"""
Map agent fields to Agent Registration API payload.
Required by API: name, description, purpose, tool
Optional fields are filled best-effort; empty/None/blank fields are pruned.
"""
name = agent.get("name") or agent.get("id") or str(agent.get("_id") or "Unnamed Agent")
description = agent.get("description") or agent.get("instructions") or f"{name} agent"
purpose = agent.get("purpose") or description
# API's "tool" = platform where agent operates; best-effort map to provider
tool = "LibreChat (chat-sandbox)"
# Optional mappings
version = agent.get("version") or agent.get("model")
creation_date = parse_iso(agent.get("createdAt"))
last_updated = parse_iso(agent.get("updatedAt"))
capabilities = ensure_list_of_str(agent.get("capabilities") or agent.get("tools"))
department = agent.get("department") or agent.get("category")
# contact_person: prefer a support contact email or name; else use author (often email)
contact_person = None
sc = agent.get("support_contact")
if isinstance(sc, dict):
contact_person = sc.get("email") or sc.get("name")
if not contact_person:
contact_person = agent.get("contact_person") or agent.get("author")
# tags: compact context about provider/model/category
tags = [t for t in [agent.get("provider"), agent.get("model"), agent.get("category")] if t]
# metadata: stash extras for traceability
avatar_path = None
avatar = agent.get("avatar")
if isinstance(avatar, dict):
avatar_path = avatar.get("filepath")
elif isinstance(avatar, str):
avatar_path = avatar
metadata = {
"source_id": agent.get("id"),
"provider": agent.get("provider"),
"model": agent.get("model"),
"artifacts": agent.get("artifacts"),
"tool_kwargs": agent.get("tool_kwargs"),
"agent_ids": agent.get("agent_ids"),
"projectIds": agent.get("projectIds"),
"avatar": avatar_path,
"author_email": agent.get("author"),
"raw_category": agent.get("category"),
}
# Prune empty metadata
metadata = {k: v for k, v in metadata.items() if v not in (None, "", [], {})}
# Extract usage data
usage_timeline = agent.get("usage_timeline", [])
usage_summary = agent.get("usage_summary", {})
payload: Dict[str, Any] = {
"name": name,
"description": description,
"purpose": purpose,
"tool": tool,
# Optionals:
"version": version,
"creation_date": creation_date,
"last_updated": last_updated,
"capabilities": capabilities,
"department": department,
"contact_person": contact_person,
"tags": tags or None,
"metadata": metadata or None,
# Usage data:
"usage_timeline": usage_timeline or None,
"conversation_count": usage_summary.get("conversation_count", 0),
"unique_users": usage_summary.get("unique_users", 0),
"total_messages": usage_summary.get("total_messages", 0),
"first_used": parse_iso(usage_summary.get("first_used")),
"last_used": parse_iso(usage_summary.get("last_used")),
}
# Final prune of empties so the API only sees meaningful data
for k in list(payload.keys()):
if payload[k] in (None, "", [], {}):
payload.pop(k, None)
return payload
def post_agent(session: requests.Session, base_url: str, api_key: str, payload: Dict[str, Any],
retries: int = 3, backoff: float = 1.5) -> Dict[str, Any]:
"""POST with simple retry on transient errors. Return parsed JSON or a structured error."""
headers = {
"Content-Type": "application/json",
"X-API-Key": api_key,
}
attempt = 0
last_err: Optional[str] = None
while attempt <= retries:
try:
resp = session.post(base_url, headers=headers, json=payload, timeout=30)
if resp.status_code >= 200 and resp.status_code < 300:
try:
return {"ok": True, "status_code": resp.status_code, "data": resp.json(), "payload_name": payload.get("name")}
except Exception:
return {"ok": True, "status_code": resp.status_code, "data": {"raw": resp.text}, "payload_name": payload.get("name")}
else:
# Retry on 429/5xx
if resp.status_code in (429, 500, 502, 503, 504) and attempt < retries:
time.sleep((attempt + 1) * backoff)
attempt += 1
continue
try:
detail = resp.json()
except Exception:
detail = {"raw": resp.text}
return {"ok": False, "status_code": resp.status_code, "error": detail, "payload_name": payload.get("name")}
except requests.RequestException as e:
last_err = str(e)
if attempt < retries:
time.sleep((attempt + 1) * backoff)
attempt += 1
continue
return {"ok": False, "status_code": None, "error": {"exception": last_err}, "payload_name": payload.get("name")}
return {"ok": False, "status_code": None, "error": {"exception": last_err or "unknown"}, "payload_name": payload.get("name")}
def load_records(path: str) -> List[Dict[str, Any]]:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, dict) and "agents" in data and isinstance(data["agents"], list):
items = data["agents"]
elif isinstance(data, list):
items = data
else:
raise ValueError("Unrecognized JSON structure. Expected a list, or an object with an 'agents' list.")
return [extract_agent(item) for item in items]
def main():
parser = argparse.ArgumentParser(description="Register agents with the Agent Registration API")
parser.add_argument("--input", "-i", default="shared_agents.json", help="Path to input JSON (default: shared_agents.json)")
parser.add_argument("--base-url", default=os.environ.get("AGENT_REG_URL", DEFAULT_BASE_URL),
help=f"API endpoint URL (default: {DEFAULT_BASE_URL})")
parser.add_argument("--api-key", default=os.environ.get("AGENT_REG_KEY", DEFAULT_API_KEY),
help="API key (default: uses static key from the docs unless AGENT_REG_KEY is set)")
parser.add_argument("--dry-run", action="store_true", help="Print payloads without sending to the API")
parser.add_argument("--save-log", default="registration_results.json", help="Path to write results log JSON")
args = parser.parse_args()
# Load items
try:
agents = load_records(args.input)
except Exception as e:
print(f"Failed to load input JSON: {e}")
sys.exit(2)
session = requests.Session()
# Disable SSL certificate verification for development/internal APIs
session.verify = False
# Create an adapter with SSL verification disabled
from requests.adapters import HTTPAdapter
from urllib3.poolmanager import PoolManager
from urllib3.util import ssl_
class SSLAdapter(HTTPAdapter):
def init_poolmanager(self, *args, **kwargs):
kwargs['ssl_context'] = ssl_.create_urllib3_context()
kwargs['ssl_context'].check_hostname = False
kwargs['ssl_context'].verify_mode = 0
return super().init_poolmanager(*args, **kwargs)
session.mount('https://', SSLAdapter())
results: List[Dict[str, Any]] = []
success = 0
usage_logged = 0
failures = 0
for idx, agent in enumerate(agents, start=1):
payload = build_payload(agent)
if args.dry_run:
print(f"[DRY RUN {idx}/{len(agents)}] Would register: {payload.get('name')}")
print(json.dumps(payload, indent=2, ensure_ascii=False))
results.append({"ok": True, "dry_run": True, "payload": payload})
continue
res = post_agent(session, args.base_url, args.api_key, payload)
results.append(res)
if res.get("ok"):
data = res.get("data", {})
status = str(data.get("status", "")).lower()
if status == "usage_logged":
usage_logged += 1
print(f"[{idx}/{len(agents)}] Usage tracked for existing agent: {payload.get('name')}")
else:
success += 1
print(f"[{idx}/{len(agents)}] Registered: {payload.get('name')}")
else:
failures += 1
print(f"[{idx}/{len(agents)}] FAILED for {payload.get('name')}: {res.get('error')}")
# Write log
try:
with open(args.save_log, "w", encoding="utf-8") as f:
json.dump(results, f, indent=2, ensure_ascii=False)
print(f"\nWrote results to {args.save_log}")
except Exception as e:
print(f"Failed to write results log: {e}")
print(f"\nSummary: registered={success}, usage_logged={usage_logged}, failed={failures}, total={len(agents)}")
if __name__ == "__main__":
main()