chore: initial commit of ai site platform
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
98
agent_sdk/client.py
Normal file
98
agent_sdk/client.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""智能体侧:解密胶囊并按契约请求平台(前端不持有明文契约)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
|
||||
|
||||
PREFIX = "AJZ1"
|
||||
|
||||
|
||||
def decrypt_capsule(agent_key_b64: str, capsule: str) -> dict[str, Any]:
|
||||
parts = capsule.split(".")
|
||||
if len(parts) != 3 or parts[0] != PREFIX:
|
||||
raise ValueError("invalid capsule")
|
||||
key = base64.urlsafe_b64decode(pad(agent_key_b64))
|
||||
nonce = base64.urlsafe_b64decode(pad(parts[1]))
|
||||
data = base64.urlsafe_b64decode(pad(parts[2]))
|
||||
# Go RawURLEncoding has no padding; Python needs pad
|
||||
aes = AESGCM(key)
|
||||
plain = aes.decrypt(nonce, data, None)
|
||||
return json.loads(plain)
|
||||
|
||||
|
||||
def pad(s: str) -> str:
|
||||
return s + "=" * (-len(s) % 4)
|
||||
|
||||
|
||||
class AgentClient:
|
||||
def __init__(self, access_token: str, agent_key: str, capsule: str):
|
||||
self.access_token = access_token
|
||||
self.desc = decrypt_capsule(agent_key, capsule)
|
||||
self.http = httpx.Client(
|
||||
base_url=self.desc["base_url"],
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
timeout=30.0,
|
||||
)
|
||||
|
||||
def resources(self) -> list[dict[str, Any]]:
|
||||
return self.desc.get("resources", [])
|
||||
|
||||
def list(self, resource_name: str, **filters: str) -> dict[str, Any]:
|
||||
res = self._find(resource_name)
|
||||
params = {f"filter.{k}": v for k, v in filters.items()}
|
||||
r = self.http.get(res["path"], params=params)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def create(self, resource_name: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||
res = self._find(resource_name)
|
||||
r = self.http.post(res["path"], json=body)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def get(self, resource_name: str, row_id: str) -> dict[str, Any]:
|
||||
res = self._find(resource_name)
|
||||
r = self.http.get(f"{res['path'].rstrip('/')}/{row_id}")
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def update(self, resource_name: str, row_id: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||
res = self._find(resource_name)
|
||||
r = self.http.put(f"{res['path'].rstrip('/')}/{row_id}", json=body)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def delete(self, resource_name: str, row_id: str) -> None:
|
||||
res = self._find(resource_name)
|
||||
r = self.http.delete(f"{res['path'].rstrip('/')}/{row_id}")
|
||||
if r.status_code not in (200, 204):
|
||||
r.raise_for_status()
|
||||
|
||||
def _find(self, name: str) -> dict[str, Any]:
|
||||
for r in self.resources():
|
||||
if r["name"] == name:
|
||||
return r
|
||||
raise KeyError(f"resource not in capsule: {name}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import sys
|
||||
|
||||
token = os.environ.get("AJZ_TOKEN", "")
|
||||
key = os.environ.get("AJZ_AGENT_KEY", "")
|
||||
capsule = os.environ.get("AJZ_CAPSULE", "")
|
||||
if not (token and key and capsule):
|
||||
print("Set AJZ_TOKEN / AJZ_AGENT_KEY / AJZ_CAPSULE", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
client = AgentClient(token, key, capsule)
|
||||
print(json.dumps({"app": client.desc["app_slug"], "resources": [r["name"] for r in client.resources()]}, ensure_ascii=False))
|
||||
if client.resources():
|
||||
name = client.resources()[0]["name"]
|
||||
print(json.dumps(client.list(name), ensure_ascii=False, indent=2))
|
||||
Reference in New Issue
Block a user