cortexdb-sdk 0.2.0b2__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.
- _cortexdb_client/__init__.py +73 -0
- _cortexdb_client/answering.py +59 -0
- _cortexdb_client/aql.py +106 -0
- _cortexdb_client/client.py +291 -0
- _cortexdb_client/errors.py +34 -0
- _cortexdb_client/generated/__init__.py +3 -0
- _cortexdb_client/generated/openapi_types.py +1305 -0
- _cortexdb_client/grounding.py +197 -0
- _cortexdb_client/model_types/__init__.py +66 -0
- _cortexdb_client/model_types/context.py +163 -0
- _cortexdb_client/model_types/core.py +187 -0
- _cortexdb_client/model_types/ingestion.py +63 -0
- _cortexdb_client/model_types/memory.py +20 -0
- _cortexdb_client/model_types/search.py +165 -0
- _cortexdb_client/model_types/verification.py +88 -0
- _cortexdb_client/models.py +73 -0
- _cortexdb_client/py.typed +1 -0
- _cortexdb_client/transport.py +94 -0
- cortexdb_client.py +75 -0
- cortexdb_sdk-0.2.0b2.dist-info/METADATA +58 -0
- cortexdb_sdk-0.2.0b2.dist-info/RECORD +23 -0
- cortexdb_sdk-0.2.0b2.dist-info/WHEEL +5 -0
- cortexdb_sdk-0.2.0b2.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from .aql import build_remember_aql, build_retrieve_context_aql, build_verify_fact_aql
|
|
2
|
+
from .client import CortexDBClient
|
|
3
|
+
from .errors import CortexDBError
|
|
4
|
+
from .generated import openapi_types
|
|
5
|
+
from .models import (
|
|
6
|
+
AnnEvaluationResponse,
|
|
7
|
+
AnnSearchReport,
|
|
8
|
+
AnswerGroundingReportResponse,
|
|
9
|
+
AnswerGroundingSpanResponse,
|
|
10
|
+
AqlCellResponse,
|
|
11
|
+
AqlQueryCacheStatsResponse,
|
|
12
|
+
AqlResponse,
|
|
13
|
+
CellLookupResponse,
|
|
14
|
+
CellResponse,
|
|
15
|
+
ContextPackAnomalyResponse,
|
|
16
|
+
ContextPackCellResponse,
|
|
17
|
+
ContextPackResponse,
|
|
18
|
+
DeleteJobResponse,
|
|
19
|
+
EvidenceResponse,
|
|
20
|
+
ExplainResponse,
|
|
21
|
+
GroundedAnswerResponse,
|
|
22
|
+
GuardResponse,
|
|
23
|
+
HealthResponse,
|
|
24
|
+
IngestResponse,
|
|
25
|
+
IngestionJobResponse,
|
|
26
|
+
NumericConflictResponse,
|
|
27
|
+
PutCellResponse,
|
|
28
|
+
RememberResponse,
|
|
29
|
+
SearchResponse,
|
|
30
|
+
SearchResult,
|
|
31
|
+
SourceRefResponse,
|
|
32
|
+
StatsResponse,
|
|
33
|
+
ValidationResponse,
|
|
34
|
+
VerificationReportResponse,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"AnnEvaluationResponse",
|
|
39
|
+
"AnnSearchReport",
|
|
40
|
+
"AnswerGroundingReportResponse",
|
|
41
|
+
"AnswerGroundingSpanResponse",
|
|
42
|
+
"AqlCellResponse",
|
|
43
|
+
"AqlQueryCacheStatsResponse",
|
|
44
|
+
"AqlResponse",
|
|
45
|
+
"CellLookupResponse",
|
|
46
|
+
"CellResponse",
|
|
47
|
+
"ContextPackAnomalyResponse",
|
|
48
|
+
"ContextPackCellResponse",
|
|
49
|
+
"ContextPackResponse",
|
|
50
|
+
"CortexDBClient",
|
|
51
|
+
"CortexDBError",
|
|
52
|
+
"DeleteJobResponse",
|
|
53
|
+
"EvidenceResponse",
|
|
54
|
+
"ExplainResponse",
|
|
55
|
+
"GroundedAnswerResponse",
|
|
56
|
+
"GuardResponse",
|
|
57
|
+
"HealthResponse",
|
|
58
|
+
"IngestResponse",
|
|
59
|
+
"IngestionJobResponse",
|
|
60
|
+
"NumericConflictResponse",
|
|
61
|
+
"PutCellResponse",
|
|
62
|
+
"RememberResponse",
|
|
63
|
+
"SearchResponse",
|
|
64
|
+
"SearchResult",
|
|
65
|
+
"SourceRefResponse",
|
|
66
|
+
"StatsResponse",
|
|
67
|
+
"ValidationResponse",
|
|
68
|
+
"VerificationReportResponse",
|
|
69
|
+
"build_remember_aql",
|
|
70
|
+
"build_retrieve_context_aql",
|
|
71
|
+
"build_verify_fact_aql",
|
|
72
|
+
"openapi_types",
|
|
73
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Callable, Protocol
|
|
4
|
+
|
|
5
|
+
from .aql import build_retrieve_context_aql, build_verify_fact_aql
|
|
6
|
+
from .grounding import _grounded_answer_response
|
|
7
|
+
from .models import ContextPackResponse, GroundedAnswerResponse, VerificationReportResponse
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GroundedAnswerClient(Protocol):
|
|
11
|
+
def context_response(self, scope: str, statement: str) -> ContextPackResponse: ...
|
|
12
|
+
|
|
13
|
+
def verify_response(self, scope: str, statement: str) -> VerificationReportResponse: ...
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def answer_with_grounded_context(
|
|
17
|
+
client: GroundedAnswerClient,
|
|
18
|
+
scope: str,
|
|
19
|
+
brain: str,
|
|
20
|
+
question: str,
|
|
21
|
+
answerer: Callable[[ContextPackResponse], str],
|
|
22
|
+
*,
|
|
23
|
+
mode: str | None = "balanced",
|
|
24
|
+
budget_tokens: int | None = None,
|
|
25
|
+
limit_candidates: int | None = None,
|
|
26
|
+
where_clause: str | None = None,
|
|
27
|
+
require_citations: bool = True,
|
|
28
|
+
reject_unsupported: bool = False,
|
|
29
|
+
verify_answer: bool = True,
|
|
30
|
+
) -> GroundedAnswerResponse:
|
|
31
|
+
retrieve_statement = build_retrieve_context_aql(
|
|
32
|
+
question,
|
|
33
|
+
brain,
|
|
34
|
+
mode=mode,
|
|
35
|
+
budget_tokens=budget_tokens,
|
|
36
|
+
limit_candidates=limit_candidates,
|
|
37
|
+
where_clause=where_clause,
|
|
38
|
+
require_citations=require_citations,
|
|
39
|
+
)
|
|
40
|
+
context = client.context_response(scope, retrieve_statement)
|
|
41
|
+
answer = answerer(context)
|
|
42
|
+
verify_statement = (
|
|
43
|
+
build_verify_fact_aql(answer, brain) if verify_answer and answer.strip() else None
|
|
44
|
+
)
|
|
45
|
+
verification = (
|
|
46
|
+
client.verify_response(scope, verify_statement)
|
|
47
|
+
if verify_statement is not None
|
|
48
|
+
else None
|
|
49
|
+
)
|
|
50
|
+
return _grounded_answer_response(
|
|
51
|
+
question=question,
|
|
52
|
+
answer=answer,
|
|
53
|
+
retrieve_statement=retrieve_statement,
|
|
54
|
+
verify_statement=verify_statement,
|
|
55
|
+
context=context,
|
|
56
|
+
verification=verification,
|
|
57
|
+
require_citations=require_citations,
|
|
58
|
+
reject_unsupported=reject_unsupported,
|
|
59
|
+
)
|
_cortexdb_client/aql.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _quote_aql_string(value: str) -> str:
|
|
5
|
+
escaped = (
|
|
6
|
+
value.replace("\\", "\\\\")
|
|
7
|
+
.replace('"', '\\"')
|
|
8
|
+
.replace("\n", "\\n")
|
|
9
|
+
.replace("\r", "\\r")
|
|
10
|
+
.replace("\t", "\\t")
|
|
11
|
+
)
|
|
12
|
+
return f'"{escaped}"'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _validate_aql_identifier(field: str, value: str) -> None:
|
|
16
|
+
if not value:
|
|
17
|
+
raise ValueError(f"{field} must be a non-empty AQL identifier")
|
|
18
|
+
first = value[0]
|
|
19
|
+
if not (first == "_" or first.isascii() and first.isalpha()):
|
|
20
|
+
raise ValueError(f"{field} must start with '_' or an ASCII letter")
|
|
21
|
+
for character in value[1:]:
|
|
22
|
+
if not (
|
|
23
|
+
character == "_"
|
|
24
|
+
or character == "-"
|
|
25
|
+
or character == ":"
|
|
26
|
+
or character.isascii()
|
|
27
|
+
and character.isalnum()
|
|
28
|
+
):
|
|
29
|
+
raise ValueError(f"{field} contains an invalid AQL identifier character")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _validate_decimal(field: str, value: str | None) -> None:
|
|
33
|
+
if value is None:
|
|
34
|
+
return
|
|
35
|
+
left, separator, right = value.partition(".")
|
|
36
|
+
if separator != "." or not left.isdigit() or not right.isdigit():
|
|
37
|
+
raise ValueError(f"{field} must be a decimal literal")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_retrieve_context_aql(
|
|
41
|
+
task: str,
|
|
42
|
+
brain: str,
|
|
43
|
+
*,
|
|
44
|
+
mode: str | None = None,
|
|
45
|
+
budget_tokens: int | None = None,
|
|
46
|
+
limit_candidates: int | None = None,
|
|
47
|
+
where_clause: str | None = None,
|
|
48
|
+
require_citations: bool = False,
|
|
49
|
+
min_confidence: str | None = None,
|
|
50
|
+
source_trust: str | None = None,
|
|
51
|
+
freshness_seconds: int | None = None,
|
|
52
|
+
explain: bool = False,
|
|
53
|
+
) -> str:
|
|
54
|
+
_validate_aql_identifier("brain", brain)
|
|
55
|
+
if mode is not None and mode not in {"fast", "balanced", "semantic", "audit"}:
|
|
56
|
+
raise ValueError("mode must be fast, balanced, semantic, or audit")
|
|
57
|
+
if where_clause is not None and not where_clause.strip():
|
|
58
|
+
raise ValueError("where_clause must not be empty")
|
|
59
|
+
_validate_decimal("min_confidence", min_confidence)
|
|
60
|
+
_validate_decimal("source_trust", source_trust)
|
|
61
|
+
|
|
62
|
+
parts = []
|
|
63
|
+
if explain:
|
|
64
|
+
parts.append("EXPLAIN")
|
|
65
|
+
parts.extend(["RETRIEVE CONTEXT FOR TASK", _quote_aql_string(task), "IN BRAIN", brain])
|
|
66
|
+
if mode is not None:
|
|
67
|
+
parts.extend(["USING MODE", mode])
|
|
68
|
+
if budget_tokens is not None:
|
|
69
|
+
parts.extend(["BUDGET", str(budget_tokens), "TOKENS"])
|
|
70
|
+
if limit_candidates is not None:
|
|
71
|
+
parts.extend(["LIMIT", str(limit_candidates), "CANDIDATES"])
|
|
72
|
+
if where_clause is not None:
|
|
73
|
+
parts.extend(["WHERE", where_clause.strip()])
|
|
74
|
+
if require_citations:
|
|
75
|
+
parts.extend(["REQUIRE", "citations"])
|
|
76
|
+
if min_confidence is not None:
|
|
77
|
+
parts.extend(["REQUIRE", "confidence", ">=", min_confidence])
|
|
78
|
+
if source_trust is not None:
|
|
79
|
+
parts.extend(["REQUIRE", "source_trust", ">=", source_trust])
|
|
80
|
+
if freshness_seconds is not None:
|
|
81
|
+
parts.extend(["REQUIRE", "freshness", "<=", str(freshness_seconds), "SECONDS"])
|
|
82
|
+
return " ".join(parts) + ";"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def build_verify_fact_aql(fact: str, brain: str) -> str:
|
|
86
|
+
_validate_aql_identifier("brain", brain)
|
|
87
|
+
return f"VERIFY FACT {_quote_aql_string(fact)} IN BRAIN {brain};"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def build_remember_aql(
|
|
91
|
+
content: str,
|
|
92
|
+
scope: str,
|
|
93
|
+
memory_type: str,
|
|
94
|
+
*,
|
|
95
|
+
ttl_seconds: int | None = None,
|
|
96
|
+
) -> str:
|
|
97
|
+
_validate_aql_identifier("scope", scope)
|
|
98
|
+
_validate_aql_identifier("memory_type", memory_type)
|
|
99
|
+
statement = (
|
|
100
|
+
f"REMEMBER {_quote_aql_string(content)} IN SCOPE {scope} AS TYPE {memory_type}"
|
|
101
|
+
)
|
|
102
|
+
if ttl_seconds is not None:
|
|
103
|
+
statement += f" TTL {ttl_seconds} SECONDS"
|
|
104
|
+
return statement + ";"
|
|
105
|
+
|
|
106
|
+
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import urllib.parse
|
|
4
|
+
from dataclasses import dataclass, field, replace
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
from .aql import build_remember_aql, build_retrieve_context_aql, build_verify_fact_aql
|
|
8
|
+
from .answering import answer_with_grounded_context as _answer_with_grounded_context
|
|
9
|
+
from .models import (
|
|
10
|
+
AnnEvaluationResponse,
|
|
11
|
+
AqlResponse,
|
|
12
|
+
CellLookupResponse,
|
|
13
|
+
ContextPackResponse,
|
|
14
|
+
DeleteJobResponse,
|
|
15
|
+
GroundedAnswerResponse,
|
|
16
|
+
HealthResponse,
|
|
17
|
+
IngestResponse,
|
|
18
|
+
IngestionJobResponse,
|
|
19
|
+
PutCellResponse,
|
|
20
|
+
RememberResponse,
|
|
21
|
+
SearchResponse,
|
|
22
|
+
StatsResponse,
|
|
23
|
+
ValidationResponse,
|
|
24
|
+
VerificationReportResponse,
|
|
25
|
+
)
|
|
26
|
+
from .transport import build_opener, close_opener, request_json, scoped_path
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class CortexDBClient:
|
|
31
|
+
base_url: str = "http://127.0.0.1:8181"
|
|
32
|
+
token: str | None = None
|
|
33
|
+
tenant: str | None = None
|
|
34
|
+
max_retries: int = 0
|
|
35
|
+
retry_delay_seconds: float = 0.5
|
|
36
|
+
timeout_seconds: float = 10.0
|
|
37
|
+
_opener: Any | None = field(default=None, repr=False, compare=False)
|
|
38
|
+
|
|
39
|
+
def with_tenant(self, tenant: str) -> "CortexDBClient":
|
|
40
|
+
return replace(self, tenant=tenant)
|
|
41
|
+
|
|
42
|
+
def with_retries(self, max_retries: int, retry_delay_seconds: float = 0.5) -> "CortexDBClient":
|
|
43
|
+
return replace(self, max_retries=max_retries, retry_delay_seconds=retry_delay_seconds)
|
|
44
|
+
|
|
45
|
+
def with_timeout(self, timeout_seconds: float) -> "CortexDBClient":
|
|
46
|
+
return replace(self, timeout_seconds=timeout_seconds)
|
|
47
|
+
|
|
48
|
+
def with_session(self) -> "CortexDBClient":
|
|
49
|
+
if self._opener is not None:
|
|
50
|
+
return self
|
|
51
|
+
return replace(self, _opener=build_opener())
|
|
52
|
+
|
|
53
|
+
def __enter__(self) -> "CortexDBClient":
|
|
54
|
+
if self._opener is None:
|
|
55
|
+
object.__setattr__(self, "_opener", build_opener())
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
def __exit__(self, exc_type: object, exc: object, tb: object) -> None:
|
|
59
|
+
self.close()
|
|
60
|
+
|
|
61
|
+
def close(self) -> None:
|
|
62
|
+
close_opener(self._opener)
|
|
63
|
+
object.__setattr__(self, "_opener", None)
|
|
64
|
+
|
|
65
|
+
build_retrieve_context_aql = staticmethod(build_retrieve_context_aql)
|
|
66
|
+
build_verify_fact_aql = staticmethod(build_verify_fact_aql)
|
|
67
|
+
build_remember_aql = staticmethod(build_remember_aql)
|
|
68
|
+
|
|
69
|
+
def health(self) -> dict[str, Any]:
|
|
70
|
+
return self._request("GET", "/v1/health", b"")
|
|
71
|
+
|
|
72
|
+
def health_response(self) -> HealthResponse:
|
|
73
|
+
return HealthResponse.from_json(self.health())
|
|
74
|
+
|
|
75
|
+
def put_cell(self, cell_id: int, payload: str) -> dict[str, Any]:
|
|
76
|
+
return self._request("POST", self._path("/v1/cell", cell_id=cell_id), payload.encode())
|
|
77
|
+
|
|
78
|
+
def put_cell_response(self, cell_id: int, payload: str) -> PutCellResponse:
|
|
79
|
+
return PutCellResponse.from_json(self.put_cell(cell_id, payload))
|
|
80
|
+
|
|
81
|
+
def get_cell(self, cell_id: int) -> dict[str, Any]:
|
|
82
|
+
return self._request("GET", self._path("/v1/cell", cell_id=cell_id), b"")
|
|
83
|
+
|
|
84
|
+
def get_cell_response(self, cell_id: int) -> CellLookupResponse:
|
|
85
|
+
return CellLookupResponse.from_json(self.get_cell(cell_id))
|
|
86
|
+
|
|
87
|
+
def tombstone_cell(self, cell_id: int) -> dict[str, Any]:
|
|
88
|
+
return self._request("DELETE", self._path("/v1/cell", cell_id=cell_id), b"")
|
|
89
|
+
|
|
90
|
+
def flush(self) -> dict[str, Any]:
|
|
91
|
+
return self._request("POST", "/v1/flush", b"")
|
|
92
|
+
|
|
93
|
+
def compact(self) -> dict[str, Any]:
|
|
94
|
+
return self._request("POST", "/v1/compact", b"")
|
|
95
|
+
|
|
96
|
+
def search(self, scope: str, query: str, limit: int = 20) -> dict[str, Any]:
|
|
97
|
+
path = self._path("/v1/search", scope=scope, mode="keyword", q=query, limit=limit)
|
|
98
|
+
return self._request("POST", path, b"")
|
|
99
|
+
|
|
100
|
+
def search_response(self, scope: str, query: str, limit: int = 20) -> SearchResponse:
|
|
101
|
+
return SearchResponse.from_json(self.search(scope, query, limit))
|
|
102
|
+
|
|
103
|
+
def search_vector(
|
|
104
|
+
self,
|
|
105
|
+
scope: str,
|
|
106
|
+
vector: list[int] | tuple[int, ...],
|
|
107
|
+
limit: int = 20,
|
|
108
|
+
algorithm: str = "ann",
|
|
109
|
+
) -> dict[str, Any]:
|
|
110
|
+
literal = ",".join(str(value) for value in vector)
|
|
111
|
+
path = self._path(
|
|
112
|
+
"/v1/search",
|
|
113
|
+
scope=scope,
|
|
114
|
+
mode="vector",
|
|
115
|
+
algorithm=algorithm,
|
|
116
|
+
vector=literal,
|
|
117
|
+
limit=limit,
|
|
118
|
+
)
|
|
119
|
+
return self._request("POST", path, b"")
|
|
120
|
+
|
|
121
|
+
def search_vector_response(
|
|
122
|
+
self,
|
|
123
|
+
scope: str,
|
|
124
|
+
vector: list[int] | tuple[int, ...],
|
|
125
|
+
limit: int = 20,
|
|
126
|
+
algorithm: str = "ann",
|
|
127
|
+
) -> SearchResponse:
|
|
128
|
+
return SearchResponse.from_json(self.search_vector(scope, vector, limit, algorithm))
|
|
129
|
+
|
|
130
|
+
def evaluate_ann(
|
|
131
|
+
self,
|
|
132
|
+
scope: str,
|
|
133
|
+
vector: list[int] | tuple[int, ...],
|
|
134
|
+
limit: int = 20,
|
|
135
|
+
) -> dict[str, Any]:
|
|
136
|
+
literal = ",".join(str(value) for value in vector)
|
|
137
|
+
path = self._path(
|
|
138
|
+
"/v1/search/ann-evaluate",
|
|
139
|
+
scope=scope,
|
|
140
|
+
vector=literal,
|
|
141
|
+
limit=limit,
|
|
142
|
+
)
|
|
143
|
+
return self._request("POST", path, b"")
|
|
144
|
+
|
|
145
|
+
def evaluate_ann_response(
|
|
146
|
+
self,
|
|
147
|
+
scope: str,
|
|
148
|
+
vector: list[int] | tuple[int, ...],
|
|
149
|
+
limit: int = 20,
|
|
150
|
+
) -> AnnEvaluationResponse:
|
|
151
|
+
return AnnEvaluationResponse.from_json(self.evaluate_ann(scope, vector, limit))
|
|
152
|
+
|
|
153
|
+
def aql(self, scope: str, statement: str) -> dict[str, Any]:
|
|
154
|
+
return self._request("POST", self._path("/v1/aql", scope=scope), statement.encode())
|
|
155
|
+
|
|
156
|
+
def aql_response(self, scope: str, statement: str) -> AqlResponse:
|
|
157
|
+
return AqlResponse.from_json(self.aql(scope, statement))
|
|
158
|
+
|
|
159
|
+
def context(self, scope: str, statement: str) -> dict[str, Any]:
|
|
160
|
+
return self._request("POST", self._path("/v1/context", scope=scope), statement.encode())
|
|
161
|
+
|
|
162
|
+
def context_response(self, scope: str, statement: str) -> ContextPackResponse:
|
|
163
|
+
return ContextPackResponse.from_json(self.context(scope, statement))
|
|
164
|
+
|
|
165
|
+
def answer_with_grounded_context(
|
|
166
|
+
self,
|
|
167
|
+
scope: str,
|
|
168
|
+
brain: str,
|
|
169
|
+
question: str,
|
|
170
|
+
answerer: Callable[[ContextPackResponse], str],
|
|
171
|
+
*,
|
|
172
|
+
mode: str | None = "balanced",
|
|
173
|
+
budget_tokens: int | None = None,
|
|
174
|
+
limit_candidates: int | None = None,
|
|
175
|
+
where_clause: str | None = None,
|
|
176
|
+
require_citations: bool = True,
|
|
177
|
+
reject_unsupported: bool = False,
|
|
178
|
+
verify_answer: bool = True,
|
|
179
|
+
) -> GroundedAnswerResponse:
|
|
180
|
+
return _answer_with_grounded_context(
|
|
181
|
+
self,
|
|
182
|
+
scope,
|
|
183
|
+
brain,
|
|
184
|
+
question,
|
|
185
|
+
answerer,
|
|
186
|
+
mode=mode,
|
|
187
|
+
budget_tokens=budget_tokens,
|
|
188
|
+
limit_candidates=limit_candidates,
|
|
189
|
+
where_clause=where_clause,
|
|
190
|
+
require_citations=require_citations,
|
|
191
|
+
reject_unsupported=reject_unsupported,
|
|
192
|
+
verify_answer=verify_answer,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def verify(self, scope: str, statement: str) -> dict[str, Any]:
|
|
196
|
+
return self._request("POST", self._path("/v1/verify", scope=scope), statement.encode())
|
|
197
|
+
|
|
198
|
+
def verify_response(self, scope: str, statement: str) -> VerificationReportResponse:
|
|
199
|
+
return VerificationReportResponse.from_json(self.verify(scope, statement))
|
|
200
|
+
|
|
201
|
+
def remember(self, scope: str, statement: str) -> dict[str, Any]:
|
|
202
|
+
return self._request("POST", self._path("/v1/remember", scope=scope), statement.encode())
|
|
203
|
+
|
|
204
|
+
def remember_response(self, scope: str, statement: str) -> RememberResponse:
|
|
205
|
+
return RememberResponse.from_json(self.remember(scope, statement))
|
|
206
|
+
|
|
207
|
+
def ingest_text(
|
|
208
|
+
self,
|
|
209
|
+
scope: str,
|
|
210
|
+
text: str,
|
|
211
|
+
source: str = "python_sdk",
|
|
212
|
+
) -> dict[str, Any]:
|
|
213
|
+
path = self._path("/v1/ingest/text", scope=scope, source=source)
|
|
214
|
+
return self._request("POST", path, text.encode())
|
|
215
|
+
|
|
216
|
+
def ingest_text_response(self, scope: str, text: str, source: str = "python_sdk") -> IngestResponse:
|
|
217
|
+
return IngestResponse.from_json(self.ingest_text(scope, text, source))
|
|
218
|
+
|
|
219
|
+
def ingest_json(
|
|
220
|
+
self,
|
|
221
|
+
scope: str,
|
|
222
|
+
document: str,
|
|
223
|
+
source: str = "python_sdk",
|
|
224
|
+
) -> dict[str, Any]:
|
|
225
|
+
path = self._path("/v1/ingest/json", scope=scope, source=source)
|
|
226
|
+
return self._request("POST", path, document.encode())
|
|
227
|
+
|
|
228
|
+
def ingest_json_response(self, scope: str, document: str, source: str = "python_sdk") -> IngestResponse:
|
|
229
|
+
return IngestResponse.from_json(self.ingest_json(scope, document, source))
|
|
230
|
+
|
|
231
|
+
def ingest_csv(
|
|
232
|
+
self,
|
|
233
|
+
scope: str,
|
|
234
|
+
document: str,
|
|
235
|
+
source: str = "python_sdk",
|
|
236
|
+
) -> dict[str, Any]:
|
|
237
|
+
path = self._path("/v1/ingest/csv", scope=scope, source=source)
|
|
238
|
+
return self._request("POST", path, document.encode())
|
|
239
|
+
|
|
240
|
+
def ingest_csv_response(self, scope: str, document: str, source: str = "python_sdk") -> IngestResponse:
|
|
241
|
+
return IngestResponse.from_json(self.ingest_csv(scope, document, source))
|
|
242
|
+
|
|
243
|
+
def ingestion_job(self, job_id: int) -> dict[str, Any]:
|
|
244
|
+
return self._request("GET", f"/v1/ingest/jobs/{job_id}", b"")
|
|
245
|
+
|
|
246
|
+
def ingestion_job_response(self, job_id: int) -> IngestionJobResponse:
|
|
247
|
+
return IngestionJobResponse.from_json(self.ingestion_job(job_id))
|
|
248
|
+
|
|
249
|
+
def delete_ingestion_job(self, job_id: int) -> DeleteJobResponse:
|
|
250
|
+
return DeleteJobResponse.from_json(
|
|
251
|
+
self._request("DELETE", f"/v1/ingest/jobs/{job_id}", b"")
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
def retry_ingestion_job(self, job_id: int) -> IngestionJobResponse:
|
|
255
|
+
return IngestionJobResponse.from_json(
|
|
256
|
+
self._request("POST", f"/v1/ingest/jobs/{job_id}/retry", b"")
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def validate(self) -> dict[str, Any]:
|
|
260
|
+
return self._request("GET", "/v1/validate", b"")
|
|
261
|
+
|
|
262
|
+
def validate_response(self) -> ValidationResponse:
|
|
263
|
+
return ValidationResponse.from_json(self.validate())
|
|
264
|
+
|
|
265
|
+
def stats(self) -> dict[str, Any]:
|
|
266
|
+
return self._request("GET", "/v1/stats", b"")
|
|
267
|
+
|
|
268
|
+
def stats_response(self) -> StatsResponse:
|
|
269
|
+
return StatsResponse.from_json(self.stats())
|
|
270
|
+
|
|
271
|
+
def _request(self, method: str, path: str, body: bytes) -> dict[str, Any]:
|
|
272
|
+
return request_json(
|
|
273
|
+
base_url=self.base_url,
|
|
274
|
+
tenant=self.tenant,
|
|
275
|
+
token=self.token,
|
|
276
|
+
timeout_seconds=self.timeout_seconds,
|
|
277
|
+
max_retries=self.max_retries,
|
|
278
|
+
retry_delay_seconds=self.retry_delay_seconds,
|
|
279
|
+
opener=self._opener,
|
|
280
|
+
method=method,
|
|
281
|
+
path=path,
|
|
282
|
+
body=body,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def _scoped(self, path: str) -> str:
|
|
286
|
+
return scoped_path(path, self.tenant)
|
|
287
|
+
|
|
288
|
+
@staticmethod
|
|
289
|
+
def _path(path: str, **query: object) -> str:
|
|
290
|
+
encoded = urllib.parse.urlencode({key: str(value) for key, value in query.items()})
|
|
291
|
+
return f"{path}?{encoded}" if encoded else path
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CortexDBError(Exception):
|
|
7
|
+
"""Typed exception raised for CortexDB HTTP errors."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
message: str,
|
|
12
|
+
code: str | None = None,
|
|
13
|
+
status: int | None = None,
|
|
14
|
+
body: str | None = None,
|
|
15
|
+
) -> None:
|
|
16
|
+
super().__init__(message)
|
|
17
|
+
self.code = code
|
|
18
|
+
self.status = status
|
|
19
|
+
self.body = body
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_response(cls, status: int, body: str) -> "CortexDBError":
|
|
23
|
+
try:
|
|
24
|
+
data = json.loads(body)
|
|
25
|
+
return cls(
|
|
26
|
+
message=str(data.get("message", body)),
|
|
27
|
+
code=str(data.get("code", "unknown")),
|
|
28
|
+
status=status,
|
|
29
|
+
body=body,
|
|
30
|
+
)
|
|
31
|
+
except (json.JSONDecodeError, KeyError):
|
|
32
|
+
return cls(message=body, code=None, status=status, body=body)
|
|
33
|
+
|
|
34
|
+
|