fix: support shared AI SDK api prefix

This commit is contained in:
cyuqei
2026-06-19 00:29:11 +08:00
parent b7073d9c5b
commit 431ad08b8d
2 changed files with 120 additions and 92 deletions
+1
View File
@@ -4,6 +4,7 @@ frontend/.next-dev-log/
frontend/node_modules/
*.pyc
__pycache__/
*.egg-info/
.DS_Store
._*
backend/.pytest_cache/
@@ -15,16 +15,18 @@ class SharedAIPlatformClient:
self,
*,
base_url: str = "http://127.0.0.1:8011",
api_key: str,
app_id: str,
timeout_seconds: float = 30.0,
client: httpx.Client | None = None,
):
self.base_url = base_url.rstrip("/")
self.api_key = api_key
self.app_id = app_id
self._owns_client = client is None
self._client = client or httpx.Client(base_url=self.base_url, timeout=timeout_seconds)
api_key: str,
app_id: str,
timeout_seconds: float = 30.0,
api_prefix: str = "/api/v0.2",
client: httpx.Client | None = None,
):
self.base_url = base_url.rstrip("/")
self.api_prefix = self._normalize_api_prefix(api_prefix)
self.api_key = api_key
self.app_id = app_id
self._owns_client = client is None
self._client = client or httpx.Client(base_url=self.base_url, timeout=timeout_seconds)
def close(self):
if self._owns_client:
@@ -36,8 +38,8 @@ class SharedAIPlatformClient:
def __exit__(self, exc_type, exc, tb):
self.close()
def list_tasks(self, context: RequestContext) -> list[dict]:
return self._request_json("GET", "/api/v0.2/tasks", context=context)["data"]
def list_tasks(self, context: RequestContext) -> list[dict]:
return self._request_json("GET", self._api_path("/tasks"), context=context)["data"]
def run_task(
self,
@@ -48,12 +50,12 @@ class SharedAIPlatformClient:
provider: str | None = None,
metadata: dict | None = None,
) -> dict:
return self._request_json(
"POST",
f"/api/v0.2/tasks/{task_key}/run",
context=context,
json_body={"inputs": inputs, "provider": provider, "metadata": metadata or {}},
)["data"]
return self._request_json(
"POST",
self._api_path(f"/tasks/{task_key}/run"),
context=context,
json_body={"inputs": inputs, "provider": provider, "metadata": metadata or {}},
)["data"]
def enqueue_task(
self,
@@ -64,38 +66,38 @@ class SharedAIPlatformClient:
provider: str | None = None,
metadata: dict | None = None,
) -> dict:
return self._request_json(
"POST",
f"/api/v0.2/tasks/{task_key}/enqueue",
context=context,
json_body={"inputs": inputs, "provider": provider, "metadata": metadata or {}},
)["data"]
def get_task_run(self, context: RequestContext, run_id: str) -> dict:
return self._request_json("GET", f"/api/v0.2/task-runs/{run_id}", context=context)["data"]
return self._request_json(
"POST",
self._api_path(f"/tasks/{task_key}/enqueue"),
context=context,
json_body={"inputs": inputs, "provider": provider, "metadata": metadata or {}},
)["data"]
def get_task_run(self, context: RequestContext, run_id: str) -> dict:
return self._request_json("GET", self._api_path(f"/task-runs/{run_id}"), context=context)["data"]
def list_jobs(self, context: RequestContext, *, status_value: str | None = None, limit: int = 50) -> list[dict]:
params = {"limit": limit}
if status_value:
params["status_value"] = status_value
return self._request_json("GET", "/api/v0.2/jobs", context=context, params=params)["data"]
return self._request_json("GET", self._api_path("/jobs"), context=context, params=params)["data"]
def list_conversations(self, context: RequestContext, *, page: int = 1, page_size: int = 20) -> dict:
return self._request_json(
"GET",
"/api/v0.2/conversations",
context=context,
params={"page": page, "page_size": page_size},
)["data"]
def get_conversation_detail(self, context: RequestContext, conversation_id: str) -> dict:
return self._request_json("GET", f"/api/v0.2/conversations/{conversation_id}", context=context)["data"]
def get_job(self, context: RequestContext, job_id: str) -> dict:
return self._request_json("GET", f"/api/v0.2/jobs/{job_id}", context=context)["data"]
def run_next_job(self, context: RequestContext) -> dict | None:
return self._request_json("POST", "/api/v0.2/jobs/run-next", context=context)["data"]["job"]
"GET",
self._api_path("/conversations"),
context=context,
params={"page": page, "page_size": page_size},
)["data"]
def get_conversation_detail(self, context: RequestContext, conversation_id: str) -> dict:
return self._request_json("GET", self._api_path(f"/conversations/{conversation_id}"), context=context)["data"]
def get_job(self, context: RequestContext, job_id: str) -> dict:
return self._request_json("GET", self._api_path(f"/jobs/{job_id}"), context=context)["data"]
def run_next_job(self, context: RequestContext) -> dict | None:
return self._request_json("POST", self._api_path("/jobs/run-next"), context=context)["data"]["job"]
def poll_job_until_terminal(self, context: RequestContext, job_id: str, options: PollOptions | None = None) -> dict:
opts = options or PollOptions()
@@ -108,16 +110,16 @@ class SharedAIPlatformClient:
raise TimeoutError(f"job {job_id} did not reach a terminal state within {opts.timeout_seconds} seconds")
time.sleep(opts.interval_seconds)
def list_knowledge_bases(self, context: RequestContext) -> dict:
return self._request_json("GET", "/api/v0.2/knowledge-bases", context=context)["data"]
def list_knowledge_bases(self, context: RequestContext) -> dict:
return self._request_json("GET", self._api_path("/knowledge-bases"), context=context)["data"]
def list_documents(self, context: RequestContext, knowledge_base_id: str, *, page: int = 1, page_size: int = 20) -> dict:
return self._request_json(
"GET",
f"/api/v0.2/knowledge-bases/{knowledge_base_id}/documents",
context=context,
params={"page": page, "page_size": page_size},
)["data"]
"GET",
self._api_path(f"/knowledge-bases/{knowledge_base_id}/documents"),
context=context,
params={"page": page, "page_size": page_size},
)["data"]
def create_knowledge_base(
self,
@@ -128,11 +130,11 @@ class SharedAIPlatformClient:
retrieval_mode: str = "hybrid",
) -> dict:
return self._request_json(
"POST",
"/api/v0.2/knowledge-bases",
context=context,
json_body={"name": name, "description": description, "retrieval_mode": retrieval_mode},
)["data"]
"POST",
self._api_path("/knowledge-bases"),
context=context,
json_body={"name": name, "description": description, "retrieval_mode": retrieval_mode},
)["data"]
def upload_document(
self,
@@ -143,11 +145,11 @@ class SharedAIPlatformClient:
mime_type: str | None,
content: bytes,
) -> dict:
response = self._client.post(
f"/api/v0.2/knowledge-bases/{knowledge_base_id}/documents",
headers=self._headers(context),
files={"file": (file_name, content, mime_type or "application/octet-stream")},
)
response = self._client.post(
self._api_path(f"/knowledge-bases/{knowledge_base_id}/documents"),
headers=self._headers(context),
files={"file": (file_name, content, mime_type or "application/octet-stream")},
)
self._raise_for_status(response)
payload = response.json()
if payload.get("code") not in (0, None):
@@ -174,10 +176,10 @@ class SharedAIPlatformClient:
}
if content is not None:
request_kwargs["files"] = {"file": (file_name or "document.txt", content, mime_type or "application/octet-stream")}
response = self._client.post(
f"/api/v0.2/knowledge-bases/{knowledge_base_id}/documents/{document_id}/retry",
**request_kwargs,
)
response = self._client.post(
self._api_path(f"/knowledge-bases/{knowledge_base_id}/documents/{document_id}/retry"),
**request_kwargs,
)
self._raise_for_status(response)
payload = response.json()
if payload.get("code") not in (0, None):
@@ -205,10 +207,10 @@ class SharedAIPlatformClient:
for record in records
]
return self._request_json(
"POST",
f"/api/v0.2/knowledge-bases/{knowledge_base_id}/sync-content",
context=context,
json_body={
"POST",
self._api_path(f"/knowledge-bases/{knowledge_base_id}/sync-content"),
context=context,
json_body={
"schema_version": schema_version,
"sync_mode": sync_mode,
"batch_id": batch_id,
@@ -219,10 +221,10 @@ class SharedAIPlatformClient:
def list_sync_jobs(self, context: RequestContext, knowledge_base_id: str) -> dict:
return self._request_json(
"GET",
f"/api/v0.2/knowledge-bases/{knowledge_base_id}/sync-jobs",
context=context,
)["data"]
"GET",
self._api_path(f"/knowledge-bases/{knowledge_base_id}/sync-jobs"),
context=context,
)["data"]
def run_legal_review(
self,
@@ -258,30 +260,55 @@ class SharedAIPlatformClient:
conversation_id: str | None = None,
) -> Iterator[SSEEvent]:
headers = self._headers(context)
with self._client.stream(
"POST",
"/api/v0.2/conversations/stream",
headers=headers,
json={
with self._client.stream(
"POST",
self._api_path("/conversations/stream"),
headers=headers,
json={
"conversation_id": conversation_id,
"message": message,
"chat_context": chat_context or {},
"provider": provider,
"template_key": template_key,
},
) as response:
self._raise_for_status(response)
for line in response.iter_lines():
if not line:
continue
if isinstance(line, bytes):
line = line.decode("utf-8")
if not line.startswith("data: "):
continue
payload = json.loads(line.removeprefix("data: ").strip())
yield SSEEvent(event_type=str(payload.get("type") or "unknown"), payload=payload)
def _request_json(
"provider": provider,
"template_key": template_key,
},
) as response:
self._raise_for_status(response)
data_lines: list[str] = []
for line in response.iter_lines():
if isinstance(line, bytes):
line = line.decode("utf-8")
line = line.rstrip("\r")
if not line:
yield from self._parse_sse_data_lines(data_lines)
data_lines = []
continue
if line.startswith("data:"):
data = line.removeprefix("data:")
if data.startswith(" "):
data = data[1:]
data_lines.append(data)
continue
if data_lines and line.lstrip().startswith(("{", "[")):
data_lines.append(line.strip())
yield from self._parse_sse_data_lines(data_lines)
@staticmethod
def _normalize_api_prefix(api_prefix: str) -> str:
normalized = (api_prefix or "/api/v0.2").strip().strip("/")
return f"/{normalized}" if normalized else "/api/v0.2"
def _api_path(self, path: str) -> str:
return f"{self.api_prefix}/{path.lstrip('/')}"
@staticmethod
def _parse_sse_data_lines(data_lines: list[str]) -> Iterator[SSEEvent]:
payload_text = "\n".join(line for line in data_lines if line).strip()
if not payload_text:
return
payload = json.loads(payload_text)
yield SSEEvent(event_type=str(payload.get("type") or "unknown"), payload=payload)
def _request_json(
self,
method: str,
path: str,