Connector calls, semantic rules, and DuckDB memory keep long runs stable, and stream retry stops chats from stalling. Co-authored-by: Cursor <cursoragent@cursor.com>
225 lines
8.0 KiB
Python
225 lines
8.0 KiB
Python
"""Product API for predefined native connectors. Users cannot define providers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from fastapi import APIRouter, HTTPException, Request
|
|
from pydantic import BaseModel, Field
|
|
|
|
from kamidso.api.deps import require_user
|
|
from kamidso.engine.serve.core import blocking_func_to_async
|
|
from kamidso.native_connectors.flags import (
|
|
didar_connector_enabled,
|
|
native_connectors_v1_enabled,
|
|
)
|
|
from kamidso.native_connectors.ux import (
|
|
connector_test_message,
|
|
public_connector,
|
|
public_provider,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _ok(data: Any) -> Dict[str, Any]:
|
|
return {"success": True, "data": data, "err_code": None, "err_msg": None}
|
|
|
|
|
|
def _native_enabled() -> bool:
|
|
return native_connectors_v1_enabled() and didar_connector_enabled()
|
|
|
|
|
|
def _service(request: Request):
|
|
from kamidso.engine.serve.connector.config import SERVE_SERVICE_COMPONENT_NAME
|
|
from kamidso.engine.serve.connector.service.service import ConnectorService
|
|
|
|
runtime = getattr(request.app.state.kamidso, "engine_runtime", None)
|
|
system_app = getattr(runtime, "system_app", None) if runtime is not None else None
|
|
if system_app is None:
|
|
raise HTTPException(status_code=503, detail="موتور اتصالها آماده نیست.")
|
|
service = system_app.get_component(
|
|
SERVE_SERVICE_COMPONENT_NAME,
|
|
ConnectorService,
|
|
default_component=None,
|
|
)
|
|
if service is None:
|
|
raise HTTPException(status_code=503, detail="موتور اتصالها آماده نیست.")
|
|
return service, system_app
|
|
|
|
|
|
def _owned(service, connector_id: str, user):
|
|
row = service.get_connector(connector_id)
|
|
if row is None or row.status == "deleted":
|
|
raise HTTPException(status_code=404, detail="اتصال پیدا نشد.")
|
|
if row.user_name != user.id:
|
|
raise HTTPException(status_code=404, detail="اتصال پیدا نشد.")
|
|
if user.workspace_id and row.sys_code and row.sys_code != user.workspace_id:
|
|
raise HTTPException(status_code=404, detail="اتصال پیدا نشد.")
|
|
if not service._is_native_type(row.connector_type):
|
|
raise HTTPException(status_code=404, detail="اتصال پیدا نشد.")
|
|
return row
|
|
|
|
|
|
class ConnectorCreateBody(BaseModel):
|
|
connector_type: str
|
|
display_name: str
|
|
credentials: Dict[str, str] = Field(default_factory=dict)
|
|
|
|
|
|
class ConnectorTestBody(BaseModel):
|
|
connector_type: str
|
|
credentials: Dict[str, str] = Field(default_factory=dict)
|
|
|
|
|
|
class ConnectorUpdateBody(BaseModel):
|
|
display_name: Optional[str] = None
|
|
credentials: Optional[Dict[str, str]] = None
|
|
|
|
|
|
def _probe_http(exc: Exception) -> HTTPException:
|
|
from kamidso.engine.serve.connector.service.service import ConnectorProbeFailed
|
|
|
|
if isinstance(exc, ConnectorProbeFailed):
|
|
public = connector_test_message(
|
|
{
|
|
"success": False,
|
|
"status_code": exc.status_code,
|
|
"error_type": exc.error_type,
|
|
}
|
|
)
|
|
return HTTPException(status_code=400, detail=public["message"])
|
|
text = str(exc)
|
|
if text.startswith("Missing credential"):
|
|
return HTTPException(status_code=400, detail="کلید API را وارد کنید.")
|
|
if "ENCRYPT_KEY" in text:
|
|
return HTTPException(
|
|
status_code=400,
|
|
detail="ذخیرهٔ امن کلید API در این محیط پیکربندی نشده است.",
|
|
)
|
|
if text == "Unknown native provider":
|
|
return HTTPException(status_code=404, detail="این اتصال در دسترس نیست.")
|
|
return HTTPException(status_code=400, detail="ارتباط با دیدار برقرار نشد.")
|
|
|
|
|
|
@router.get("/connectors/providers")
|
|
async def list_providers(request: Request):
|
|
require_user(request)
|
|
if not _native_enabled():
|
|
return _ok([])
|
|
from kamidso.native_connectors.registry import get_registry
|
|
|
|
registry = get_registry()
|
|
return _ok([public_provider(registry.get(item)) for item in registry.provider_ids()])
|
|
|
|
|
|
@router.get("/connectors")
|
|
async def list_user_connectors(request: Request):
|
|
user = require_user(request)
|
|
if not _native_enabled():
|
|
return _ok([])
|
|
service, _system = _service(request)
|
|
rows = service.list_connectors(
|
|
user_name=user.id,
|
|
sys_code=user.workspace_id or None,
|
|
)
|
|
visible: List[Dict[str, Any]] = []
|
|
for row in rows:
|
|
if row.status == "deleted" or not service._is_native_type(row.connector_type):
|
|
continue
|
|
visible.append(public_connector(row))
|
|
return _ok(visible)
|
|
|
|
|
|
@router.post("/connectors/test")
|
|
async def test_new_connector(body: ConnectorTestBody, request: Request):
|
|
require_user(request)
|
|
if not _native_enabled():
|
|
raise HTTPException(status_code=404, detail="این اتصال در دسترس نیست.")
|
|
service, _system = _service(request)
|
|
try:
|
|
raw = await service.test_native_credentials(body.connector_type, body.credentials)
|
|
except Exception as exc:
|
|
logger.exception("Native connector probe failed")
|
|
raise _probe_http(exc) from exc
|
|
return _ok(connector_test_message(raw))
|
|
|
|
|
|
@router.post("/connectors")
|
|
async def create_user_connector(body: ConnectorCreateBody, request: Request):
|
|
user = require_user(request)
|
|
if not _native_enabled():
|
|
raise HTTPException(status_code=404, detail="این اتصال در دسترس نیست.")
|
|
from kamidso.engine.serve.connector.service.service import ConnectorCreateRequest
|
|
|
|
service, system_app = _service(request)
|
|
if not service._is_native_type(body.connector_type):
|
|
raise HTTPException(status_code=404, detail="این اتصال در دسترس نیست.")
|
|
request_model = ConnectorCreateRequest(
|
|
connector_type=body.connector_type,
|
|
display_name=body.display_name.strip() or "دیدار CRM",
|
|
credentials=dict(body.credentials),
|
|
user_name=user.id,
|
|
sys_code=user.workspace_id or None,
|
|
)
|
|
try:
|
|
created = await blocking_func_to_async(
|
|
system_app, service.create_connector, request_model
|
|
)
|
|
except Exception as exc:
|
|
logger.exception("Create native connector failed")
|
|
raise _probe_http(exc) from exc
|
|
return _ok(public_connector(created))
|
|
|
|
|
|
@router.patch("/connectors/{connector_id}")
|
|
async def update_user_connector(
|
|
connector_id: str, body: ConnectorUpdateBody, request: Request
|
|
):
|
|
user = require_user(request)
|
|
from kamidso.engine.serve.connector.service.service import ConnectorUpdateRequest
|
|
|
|
service, system_app = _service(request)
|
|
_owned(service, connector_id, user)
|
|
credentials = None
|
|
if body.credentials:
|
|
filled = {
|
|
key: value
|
|
for key, value in body.credentials.items()
|
|
if str(value).strip()
|
|
}
|
|
credentials = filled or None
|
|
update = ConnectorUpdateRequest(
|
|
display_name=body.display_name.strip() if body.display_name else None,
|
|
credentials=credentials,
|
|
)
|
|
try:
|
|
updated = await blocking_func_to_async(
|
|
system_app, service.update_connector, connector_id, update
|
|
)
|
|
except Exception as exc:
|
|
logger.exception("Update native connector failed")
|
|
raise _probe_http(exc) from exc
|
|
return _ok(public_connector(updated))
|
|
|
|
|
|
@router.delete("/connectors/{connector_id}")
|
|
async def delete_user_connector(connector_id: str, request: Request):
|
|
user = require_user(request)
|
|
service, system_app = _service(request)
|
|
_owned(service, connector_id, user)
|
|
await blocking_func_to_async(system_app, service.retire_connector, connector_id)
|
|
return _ok({"ok": True})
|
|
|
|
|
|
@router.post("/connectors/{connector_id}/test")
|
|
async def test_saved_connector(connector_id: str, request: Request):
|
|
user = require_user(request)
|
|
service, _system = _service(request)
|
|
_owned(service, connector_id, user)
|
|
raw = await service.test_connection(connector_id)
|
|
return _ok(connector_test_message(raw))
|