Files
kamidso-back/kamidso/api/routes/connectors.py
T
javadandCursor edbaa2acec Add native Didar connectors so chats can query CRM data reliably.
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>
2026-10-12 19:26:47 +03:30

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))