smart-knowledge 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,90 @@
1
+ """Official Python 3.10+ SDK for the Smart Knowledge 5-Brain Autonomous Platform.
2
+
3
+ Provides direct programmatic access to:
4
+ - ask(): Grounded 5-Brain RAG Question Answering with primary citations.
5
+ - search(): Multi-Modal Hybrid Search across Knowledge Graph, BM25, and semantic vectors.
6
+ - agentask: Autonomous Problem Solver (PAOA) multi-step reasoning missions.
7
+ - documents: Enterprise document ingestion pipeline with full pagination.
8
+ """
9
+
10
+ from .client import SmartKnowledge
11
+ from .config import ResolvedConfig
12
+ from .errors import (
13
+ APIConnectionError,
14
+ AuthenticationError,
15
+ InternalServerError,
16
+ NotFoundError,
17
+ PermissionDeniedError,
18
+ QuotaExceededError,
19
+ RateLimitError,
20
+ SmartKnowledgeError,
21
+ ValidationError,
22
+ )
23
+ from .types import (
24
+ AgentTaskProgress,
25
+ AgentaskDispatchOptions,
26
+ AgentaskDispatchResponse,
27
+ AgentaskResult,
28
+ AgentaskRunOptions,
29
+ AgentaskStatusResponse,
30
+ AskOptions,
31
+ AskResponse,
32
+ Citation,
33
+ ClientOptions,
34
+ DocumentStatusResponse,
35
+ DocumentUploadOptions,
36
+ DocumentUploadResponse,
37
+ FileListOptions,
38
+ FileRecord,
39
+ FileUploadInput,
40
+ FinOpsMetrics,
41
+ GraphTriplet,
42
+ PaginatedFilesResponse,
43
+ PaginationMeta,
44
+ RequestOptions,
45
+ SearchOptions,
46
+ SearchResponse,
47
+ VectorHit,
48
+ Bm25Hit,
49
+ )
50
+
51
+ __version__ = "1.1.0"
52
+
53
+ __all__ = [
54
+ "SmartKnowledge",
55
+ "ResolvedConfig",
56
+ "SmartKnowledgeError",
57
+ "AuthenticationError",
58
+ "PermissionDeniedError",
59
+ "NotFoundError",
60
+ "ValidationError",
61
+ "RateLimitError",
62
+ "QuotaExceededError",
63
+ "InternalServerError",
64
+ "APIConnectionError",
65
+ "ClientOptions",
66
+ "FinOpsMetrics",
67
+ "RequestOptions",
68
+ "AskOptions",
69
+ "AskResponse",
70
+ "Citation",
71
+ "SearchOptions",
72
+ "SearchResponse",
73
+ "GraphTriplet",
74
+ "VectorHit",
75
+ "Bm25Hit",
76
+ "AgentTaskProgress",
77
+ "AgentaskDispatchOptions",
78
+ "AgentaskDispatchResponse",
79
+ "AgentaskStatusResponse",
80
+ "AgentaskRunOptions",
81
+ "AgentaskResult",
82
+ "FileUploadInput",
83
+ "FileRecord",
84
+ "DocumentUploadOptions",
85
+ "DocumentUploadResponse",
86
+ "DocumentStatusResponse",
87
+ "PaginationMeta",
88
+ "FileListOptions",
89
+ "PaginatedFilesResponse",
90
+ ]
@@ -0,0 +1,88 @@
1
+ """SmartKnowledge primary client class for Python SDK."""
2
+
3
+ from typing import Any, Dict, Optional, Union
4
+ from .config import ResolvedConfig, resolve_config
5
+ from .http import HttpClient
6
+ from .modules.agentask import AgentaskModule
7
+ from .modules.ask import AskModule
8
+ from .modules.documents import DocumentsModule
9
+ from .modules.search import SearchModule
10
+ from .types.ask import AskOptions, AskResponse
11
+ from .types.search import SearchOptions, SearchResponse
12
+
13
+
14
+ class SmartKnowledge:
15
+ """Official Python client for the Smart Knowledge 5-Brain Autonomous Platform."""
16
+
17
+ def __init__(
18
+ self,
19
+ api_key: Optional[str] = None,
20
+ base_url: Optional[str] = None,
21
+ tenant_id: Optional[str] = None,
22
+ timeout: Optional[float] = None,
23
+ max_retries: Optional[int] = None,
24
+ default_headers: Optional[Dict[str, str]] = None,
25
+ ) -> None:
26
+ """Initialize the Smart Knowledge client.
27
+
28
+ :param api_key: Smart Knowledge API key (defaults to SMART_KNOWLEDGE_API_KEY environment variable).
29
+ :param base_url: Base URL of the API gateway (defaults to SMART_KNOWLEDGE_BASE_URL or 'https://ztrust.eu').
30
+ :param tenant_id: Target tenant workspace identifier (defaults to SMART_KNOWLEDGE_TENANT_ID or 'default').
31
+ :param timeout: Default request timeout in seconds (defaults to 60.0).
32
+ :param max_retries: Maximum automatic retry attempts on 429/5xx status codes (defaults to 2).
33
+ :param default_headers: Optional dictionary of headers to attach to every outgoing request.
34
+ """
35
+ self.config: ResolvedConfig = resolve_config(
36
+ api_key=api_key,
37
+ base_url=base_url,
38
+ tenant_id=tenant_id,
39
+ timeout=timeout,
40
+ max_retries=max_retries,
41
+ default_headers=default_headers,
42
+ )
43
+ self.http = HttpClient(self.config)
44
+
45
+ self._ask_module = AskModule(self.http)
46
+ self._search_module = SearchModule(self.http)
47
+
48
+ # Autonomous Problem Solver (PAOA) module
49
+ self.agentask = AgentaskModule(self.http)
50
+
51
+ # Enterprise Document Ingestion and Library module
52
+ self.documents = DocumentsModule(self.http)
53
+
54
+ def ask(
55
+ self,
56
+ question_or_options: Optional[Union[str, AskOptions, Dict[str, Any]]] = None,
57
+ question: Optional[str] = None,
58
+ include_sources: bool = True,
59
+ timeout: Optional[float] = None,
60
+ options: Optional[Union[str, AskOptions, Dict[str, Any]]] = None,
61
+ ) -> AskResponse:
62
+ """Ask a question verified and grounded across the 5-Brain architecture."""
63
+ return self._ask_module.ask(
64
+ question_or_options=question_or_options,
65
+ question=question,
66
+ include_sources=include_sources,
67
+ timeout=timeout,
68
+ options=options,
69
+ )
70
+
71
+ def search(
72
+ self,
73
+ query_or_options: Optional[Union[str, SearchOptions, Dict[str, Any]]] = None,
74
+ query: Optional[str] = None,
75
+ decompose: bool = False,
76
+ limit: int = 10,
77
+ timeout: Optional[float] = None,
78
+ options: Optional[Union[str, SearchOptions, Dict[str, Any]]] = None,
79
+ ) -> SearchResponse:
80
+ """Multi-modal retrieval across Knowledge Graph triplets, BM25 keyword index, and semantic vectors."""
81
+ return self._search_module.search(
82
+ query_or_options=query_or_options,
83
+ query=query,
84
+ decompose=decompose,
85
+ limit=limit,
86
+ timeout=timeout,
87
+ options=options,
88
+ )
@@ -0,0 +1,63 @@
1
+ """Configuration resolver for the Smart Knowledge Python SDK."""
2
+
3
+ import os
4
+ from dataclasses import dataclass, field
5
+ from typing import Dict, Optional
6
+ from .errors import ValidationError
7
+
8
+ DEFAULT_BASE_URL = "https://ztrust.eu"
9
+ DEFAULT_TENANT_ID = "default"
10
+ DEFAULT_TIMEOUT_SEC = 60.0
11
+ DEFAULT_MAX_RETRIES = 2
12
+
13
+
14
+ @dataclass
15
+ class ResolvedConfig:
16
+ api_key: str
17
+ base_url: str = DEFAULT_BASE_URL
18
+ tenant_id: str = DEFAULT_TENANT_ID
19
+ timeout: float = DEFAULT_TIMEOUT_SEC
20
+ max_retries: int = DEFAULT_MAX_RETRIES
21
+ default_headers: Dict[str, str] = field(default_factory=dict)
22
+
23
+
24
+ def resolve_config(
25
+ api_key: Optional[str] = None,
26
+ base_url: Optional[str] = None,
27
+ tenant_id: Optional[str] = None,
28
+ timeout: Optional[float] = None,
29
+ max_retries: Optional[int] = None,
30
+ default_headers: Optional[Dict[str, str]] = None,
31
+ ) -> ResolvedConfig:
32
+ """Resolve and validate configuration options with environment fallbacks."""
33
+
34
+ resolved_key = (api_key or os.environ.get("SMART_KNOWLEDGE_API_KEY") or "").strip()
35
+ if not resolved_key:
36
+ raise ValidationError(
37
+ "Missing required API key. Pass 'api_key' to SmartKnowledge() or export SMART_KNOWLEDGE_API_KEY."
38
+ )
39
+
40
+ raw_base_url = (base_url or os.environ.get("SMART_KNOWLEDGE_BASE_URL") or DEFAULT_BASE_URL).strip()
41
+ resolved_base_url = raw_base_url.rstrip("/")
42
+
43
+ resolved_tenant = (tenant_id or os.environ.get("SMART_KNOWLEDGE_TENANT_ID") or DEFAULT_TENANT_ID).strip()
44
+ resolved_timeout = float(timeout if timeout is not None else DEFAULT_TIMEOUT_SEC)
45
+ resolved_retries = int(max_retries if max_retries is not None else DEFAULT_MAX_RETRIES)
46
+
47
+ headers = {
48
+ "Authorization": f"Bearer {resolved_key}",
49
+ "X-API-Key": resolved_key,
50
+ "X-Tenant-Id": resolved_tenant,
51
+ "User-Agent": "smart-knowledge-python-sdk/1.1.0",
52
+ }
53
+ if default_headers:
54
+ headers.update(default_headers)
55
+
56
+ return ResolvedConfig(
57
+ api_key=resolved_key,
58
+ base_url=resolved_base_url,
59
+ tenant_id=resolved_tenant,
60
+ timeout=resolved_timeout,
61
+ max_retries=resolved_retries,
62
+ default_headers=headers,
63
+ )
@@ -0,0 +1,130 @@
1
+ """Custom typed error hierarchy for the Smart Knowledge SDK."""
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+
6
+ class SmartKnowledgeError(Exception):
7
+ """Base exception class for all Smart Knowledge errors."""
8
+
9
+ def __init__(
10
+ self,
11
+ message: str,
12
+ status_code: Optional[int] = None,
13
+ response_body: Optional[Any] = None,
14
+ headers: Optional[Dict[str, str]] = None,
15
+ ) -> None:
16
+ super().__init__(message)
17
+ self.message = message
18
+ self.status_code = status_code
19
+ self.response_body = response_body
20
+ self.headers = headers or {}
21
+
22
+ def __str__(self) -> str:
23
+ if self.status_code:
24
+ return f"[{self.__class__.__name__} HTTP {self.status_code}] {self.message}"
25
+ return f"[{self.__class__.__name__}] {self.message}"
26
+
27
+
28
+ class AuthenticationError(SmartKnowledgeError):
29
+ """Raised when authentication fails (HTTP 401: Invalid or expired API key)."""
30
+
31
+ def __init__(
32
+ self,
33
+ message: str = "Invalid, revoked, or missing Smart Knowledge API key.",
34
+ status_code: int = 401,
35
+ response_body: Optional[Any] = None,
36
+ headers: Optional[Dict[str, str]] = None,
37
+ ) -> None:
38
+ super().__init__(message, status_code=status_code, response_body=response_body, headers=headers)
39
+
40
+
41
+ class PermissionDeniedError(SmartKnowledgeError):
42
+ """Raised when access is forbidden (HTTP 403: Role or tenant permission denied)."""
43
+
44
+ def __init__(
45
+ self,
46
+ message: str = "Forbidden: Caller does not possess required role or tenant permission.",
47
+ status_code: int = 403,
48
+ response_body: Optional[Any] = None,
49
+ headers: Optional[Dict[str, str]] = None,
50
+ ) -> None:
51
+ super().__init__(message, status_code=status_code, response_body=response_body, headers=headers)
52
+
53
+
54
+ class NotFoundError(SmartKnowledgeError):
55
+ """Raised when a requested resource (document, task, tenant) does not exist (HTTP 404)."""
56
+
57
+ def __init__(
58
+ self,
59
+ message: str = "The requested resource was not found.",
60
+ status_code: int = 404,
61
+ response_body: Optional[Any] = None,
62
+ headers: Optional[Dict[str, str]] = None,
63
+ ) -> None:
64
+ super().__init__(message, status_code=status_code, response_body=response_body, headers=headers)
65
+
66
+
67
+ class ValidationError(SmartKnowledgeError):
68
+ """Raised when invalid request parameters or missing required arguments are supplied."""
69
+
70
+ def __init__(
71
+ self,
72
+ message: str,
73
+ status_code: int = 400,
74
+ response_body: Optional[Any] = None,
75
+ headers: Optional[Dict[str, str]] = None,
76
+ ) -> None:
77
+ super().__init__(message, status_code=status_code, response_body=response_body, headers=headers)
78
+
79
+
80
+ class RateLimitError(SmartKnowledgeError):
81
+ """Raised when the rate limit is exceeded (HTTP 429). Includes retry_after_sec."""
82
+
83
+ def __init__(
84
+ self,
85
+ message: str = "Rate limit exceeded. Please throttle your request frequency.",
86
+ retry_after_sec: Optional[float] = None,
87
+ status_code: int = 429,
88
+ response_body: Optional[Any] = None,
89
+ headers: Optional[Dict[str, str]] = None,
90
+ ) -> None:
91
+ super().__init__(message, status_code=status_code, response_body=response_body, headers=headers)
92
+ self.retry_after_sec = retry_after_sec
93
+
94
+
95
+ class QuotaExceededError(SmartKnowledgeError):
96
+ """Raised when the tenant's monthly token or task quota is exhausted (HTTP 429)."""
97
+
98
+ def __init__(
99
+ self,
100
+ message: str = "Monthly LLM token quota exceeded for this tenant workspace.",
101
+ status_code: int = 429,
102
+ response_body: Optional[Any] = None,
103
+ headers: Optional[Dict[str, str]] = None,
104
+ ) -> None:
105
+ super().__init__(message, status_code=status_code, response_body=response_body, headers=headers)
106
+
107
+
108
+ class InternalServerError(SmartKnowledgeError):
109
+ """Raised when the server encounters an internal error (HTTP 500/502/503/504)."""
110
+
111
+ def __init__(
112
+ self,
113
+ message: str = "Internal server error occurred within Smart Knowledge platform.",
114
+ status_code: int = 500,
115
+ response_body: Optional[Any] = None,
116
+ headers: Optional[Dict[str, str]] = None,
117
+ ) -> None:
118
+ super().__init__(message, status_code=status_code, response_body=response_body, headers=headers)
119
+
120
+
121
+ class APIConnectionError(SmartKnowledgeError):
122
+ """Raised when an HTTP connection to the Smart Knowledge server fails or times out."""
123
+
124
+ def __init__(
125
+ self,
126
+ message: str = "Failed to establish a network connection to Smart Knowledge API.",
127
+ cause: Optional[Exception] = None,
128
+ ) -> None:
129
+ super().__init__(message)
130
+ self.__cause__ = cause
@@ -0,0 +1,163 @@
1
+ """HTTP transport layer for the Smart Knowledge Python SDK."""
2
+
3
+ import random
4
+ import time
5
+ from typing import Any, Dict, Optional, Union
6
+ import requests
7
+ from .config import ResolvedConfig
8
+ from .errors import (
9
+ APIConnectionError,
10
+ AuthenticationError,
11
+ InternalServerError,
12
+ NotFoundError,
13
+ PermissionDeniedError,
14
+ QuotaExceededError,
15
+ RateLimitError,
16
+ SmartKnowledgeError,
17
+ ValidationError,
18
+ )
19
+
20
+
21
+ class HttpClient:
22
+ """Connection-pooled, resilient HTTP transport layer."""
23
+
24
+ def __init__(self, config: ResolvedConfig) -> None:
25
+ self.config = config
26
+ self.session = requests.Session()
27
+ self.session.headers.update(config.default_headers)
28
+
29
+ def build_url(self, path: str) -> str:
30
+ clean_path = path if path.startswith("/") else f"/{path}"
31
+ return f"{self.config.base_url}{clean_path}"
32
+
33
+ def request(
34
+ self,
35
+ method: str,
36
+ path: str,
37
+ params: Optional[Dict[str, Any]] = None,
38
+ json_data: Optional[Any] = None,
39
+ data: Optional[Any] = None,
40
+ files: Optional[Any] = None,
41
+ headers: Optional[Dict[str, str]] = None,
42
+ timeout: Optional[float] = None,
43
+ ) -> Any:
44
+ url = self.build_url(path)
45
+ effective_timeout = timeout if timeout is not None else self.config.timeout
46
+ req_headers = dict(self.session.headers)
47
+ if headers:
48
+ req_headers.update(headers)
49
+
50
+ # Filter None params
51
+ filtered_params = {k: v for k, v in (params or {}).items() if v is not None} or None
52
+
53
+ attempt = 0
54
+ max_retries = self.config.max_retries
55
+
56
+ while True:
57
+ attempt += 1
58
+ try:
59
+ response = self.session.request(
60
+ method=method.upper(),
61
+ url=url,
62
+ params=filtered_params,
63
+ json=json_data,
64
+ data=data,
65
+ files=files,
66
+ headers=req_headers,
67
+ timeout=effective_timeout,
68
+ )
69
+
70
+ if response.status_code == 204:
71
+ return None
72
+
73
+ if 200 <= response.status_code < 300:
74
+ content_type = response.headers.get("content-type", "")
75
+ if "application/json" in content_type:
76
+ return response.json()
77
+ return response.text
78
+
79
+ # Parse error body
80
+ error_body: Any = None
81
+ try:
82
+ error_body = response.json()
83
+ except Exception:
84
+ error_body = response.text
85
+
86
+ error_msg = self._extract_error_message(error_body) or f"HTTP {response.status_code}: {response.reason}"
87
+
88
+ # Check retryable status codes
89
+ if response.status_code in (429, 502, 503, 504) and attempt <= max_retries:
90
+ retry_after = self._get_retry_after(response.headers, attempt)
91
+ time.sleep(retry_after)
92
+ continue
93
+
94
+ self._raise_for_status(response.status_code, error_msg, error_body, dict(response.headers))
95
+
96
+ except requests.exceptions.Timeout as e:
97
+ if attempt <= max_retries:
98
+ time.sleep(1.0 * (2 ** (attempt - 1)) + random.uniform(0.1, 0.5))
99
+ continue
100
+ raise APIConnectionError(f"Request timed out after {effective_timeout}s: {e}", cause=e)
101
+
102
+ except requests.exceptions.RequestException as e:
103
+ if isinstance(e, requests.exceptions.HTTPError):
104
+ # Already handled above
105
+ raise
106
+ if attempt <= max_retries:
107
+ time.sleep(1.0 * (2 ** (attempt - 1)) + random.uniform(0.1, 0.5))
108
+ continue
109
+ raise APIConnectionError(f"Network connection failed: {e}", cause=e)
110
+
111
+ def _extract_error_message(self, body: Any) -> Optional[str]:
112
+ if isinstance(body, dict):
113
+ if "error" in body:
114
+ err = body["error"]
115
+ if isinstance(err, str):
116
+ return err
117
+ if isinstance(err, dict) and "message" in err:
118
+ return str(err["message"])
119
+ if "message" in body:
120
+ return str(body["message"])
121
+ if "detail" in body:
122
+ return str(body["detail"])
123
+ elif isinstance(body, str) and body.strip():
124
+ return body.strip()
125
+ return None
126
+
127
+ def _get_retry_after(self, headers: Any, attempt: int) -> float:
128
+ if "Retry-After" in headers:
129
+ try:
130
+ return float(headers["Retry-After"])
131
+ except ValueError:
132
+ pass
133
+ backoff = 1.0 * (2 ** (attempt - 1))
134
+ jitter = random.uniform(0.1, 0.5)
135
+ return min(backoff + jitter, 15.0)
136
+
137
+ def _raise_for_status(
138
+ self,
139
+ status_code: int,
140
+ message: str,
141
+ body: Any,
142
+ headers: Dict[str, str],
143
+ ) -> None:
144
+ if status_code == 401:
145
+ raise AuthenticationError(message, status_code=status_code, response_body=body, headers=headers)
146
+ elif status_code == 403:
147
+ raise PermissionDeniedError(message, status_code=status_code, response_body=body, headers=headers)
148
+ elif status_code == 404:
149
+ raise NotFoundError(message, status_code=status_code, response_body=body, headers=headers)
150
+ elif status_code == 400:
151
+ raise ValidationError(message, status_code=status_code, response_body=body, headers=headers)
152
+ elif status_code == 429:
153
+ lower_msg = message.lower()
154
+ if "quota" in lower_msg or "monthly" in lower_msg:
155
+ raise QuotaExceededError(message, status_code=status_code, response_body=body, headers=headers)
156
+ retry_sec = self._get_retry_after(headers, 1)
157
+ raise RateLimitError(
158
+ message, retry_after_sec=retry_sec, status_code=status_code, response_body=body, headers=headers
159
+ )
160
+ elif status_code >= 500:
161
+ raise InternalServerError(message, status_code=status_code, response_body=body, headers=headers)
162
+ else:
163
+ raise SmartKnowledgeError(message, status_code=status_code, response_body=body, headers=headers)
@@ -0,0 +1,13 @@
1
+ """Modules for Smart Knowledge Python SDK."""
2
+
3
+ from .ask import AskModule
4
+ from .search import SearchModule
5
+ from .agentask import AgentaskModule
6
+ from .documents import DocumentsModule
7
+
8
+ __all__ = [
9
+ "AskModule",
10
+ "SearchModule",
11
+ "AgentaskModule",
12
+ "DocumentsModule",
13
+ ]