c889a57b6b
Test Suites / Build CI Environment (push) Has been cancelled
Test Suites / Basic Tests (push) Has been cancelled
Test Suites / End-to-End Tests (push) Has been cancelled
Test Suites / CLI Tests (push) Has been cancelled
Test Suites / Slow End-to-End Tests (push) Has been cancelled
Test Suites / Graph Database Tests (push) Has been cancelled
Test Suites / Vector DB Tests (push) Has been cancelled
Test Suites / Temporal Graph Test (push) Has been cancelled
Test Suites / Search Test on Different DBs (push) Has been cancelled
Test Suites / Example Tests (push) Has been cancelled
Test Suites / Notebook Tests (push) Has been cancelled
Test Suites / OS and Python Tests Ubuntu (push) Has been cancelled
Test Suites / OS and Python Tests Extended (push) Has been cancelled
Test Suites / LLM Test Suite (push) Has been cancelled
Test Suites / S3 File Storage Test (push) Has been cancelled
Test Suites / Run Integration Tests (push) Has been cancelled
Test Suites / MCP Tests (push) Has been cancelled
Test Suites / Docker Compose Test (push) Has been cancelled
Test Suites / Docker CI test (push) Has been cancelled
Test Suites / Relational DB Migration Tests (push) Has been cancelled
Test Suites / Distributed Cognee Test (push) Has been cancelled
Test Suites / DB Examples Tests (push) Has been cancelled
Test Suites / Test Completion Status (push) Has been cancelled
Test Suites / Claude Code Review (push) Has been cancelled
Test Suites / basic checks (push) Has been cancelled
build | Build and Push Cognee MCP Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
build | Build and Push Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.11) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.12) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (kuzu, kuzu) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (neo4j, neo4j) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Examples (push) Has been cancelled
Weighted Edges Tests / Code Quality for Weighted Edges (push) Has been cancelled
364 lines
12 KiB
Python
364 lines
12 KiB
Python
"""Thin HTTP client that delegates CLI commands to a running Cognee API server.
|
|
|
|
When ``--api-url`` is supplied (e.g. ``--api-url http://localhost:8000``),
|
|
every CLI command is forwarded as an HTTP request to the server instead of
|
|
being executed in-process. This avoids file-based database locking issues
|
|
(SQLite, Ladybug, LanceDB) that arise when multiple CLI processes try to
|
|
access the same files concurrently.
|
|
|
|
The single API server process owns all database connections and serialises
|
|
writes, which is the correct model for file-based backends.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import mimetypes
|
|
import os
|
|
from typing import Any, Optional
|
|
from urllib.parse import urljoin
|
|
|
|
|
|
def _import_httpx():
|
|
"""Import httpx lazily so the dependency is optional."""
|
|
try:
|
|
import httpx
|
|
|
|
return httpx
|
|
except ImportError:
|
|
raise SystemExit(
|
|
"The 'httpx' package is required for --api-url mode. "
|
|
"Install it with: uv pip install httpx"
|
|
)
|
|
|
|
|
|
class CogneeApiClient:
|
|
"""Wrapper around the Cognee REST API with a shared connection pool."""
|
|
|
|
def __init__(
|
|
self,
|
|
base_url: str,
|
|
timeout: float = 120.0,
|
|
headers: Optional[dict[str, str]] = None,
|
|
):
|
|
self.base_url = base_url.rstrip("/")
|
|
self.timeout = timeout
|
|
self._extra_headers = headers or {}
|
|
self._client = None
|
|
|
|
# -- lifecycle -------------------------------------------------------
|
|
|
|
def _get_client(self):
|
|
if self._client is None:
|
|
httpx = _import_httpx()
|
|
self._client = httpx.Client(
|
|
timeout=self.timeout,
|
|
headers=self._extra_headers,
|
|
)
|
|
return self._client
|
|
|
|
def close(self) -> None:
|
|
if self._client is not None:
|
|
self._client.close()
|
|
self._client = None
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *exc):
|
|
self.close()
|
|
|
|
# -- helpers ---------------------------------------------------------
|
|
|
|
def _url(self, path: str) -> str:
|
|
return urljoin(self.base_url + "/", path.lstrip("/"))
|
|
|
|
def _raise_for_status(self, resp) -> None:
|
|
if resp.status_code >= 400:
|
|
try:
|
|
detail = resp.json()
|
|
except Exception:
|
|
detail = resp.text
|
|
raise RuntimeError(f"API error {resp.status_code}: {detail}")
|
|
|
|
# -- probes ----------------------------------------------------------
|
|
|
|
def health(self) -> dict:
|
|
"""Probe the server. Uses a short timeout independent of self.timeout."""
|
|
httpx = _import_httpx()
|
|
with httpx.Client(timeout=5.0) as c:
|
|
r = c.get(self._url("/health"))
|
|
self._raise_for_status(r)
|
|
return r.json()
|
|
|
|
# -- add -------------------------------------------------------------
|
|
|
|
def add(
|
|
self,
|
|
data_items: list[str],
|
|
dataset_name: str = "main_dataset",
|
|
) -> dict:
|
|
files = []
|
|
opened = []
|
|
try:
|
|
for item in data_items:
|
|
if os.path.isfile(item):
|
|
mime, _ = mimetypes.guess_type(item)
|
|
fh = open(item, "rb") # noqa: SIM115
|
|
opened.append(fh)
|
|
files.append(
|
|
("data", (os.path.basename(item), fh, mime or "application/octet-stream"))
|
|
)
|
|
else:
|
|
files.append(
|
|
(
|
|
"data",
|
|
(f"text_{len(files)}.txt", io.BytesIO(item.encode()), "text/plain"),
|
|
)
|
|
)
|
|
form_data = {"datasetName": dataset_name}
|
|
r = self._get_client().post(self._url("/api/v1/add"), files=files, data=form_data)
|
|
self._raise_for_status(r)
|
|
return r.json()
|
|
finally:
|
|
for fh in opened:
|
|
fh.close()
|
|
|
|
# -- cognify ---------------------------------------------------------
|
|
|
|
def cognify(
|
|
self,
|
|
datasets: Optional[list[str]] = None,
|
|
run_in_background: bool = False,
|
|
chunks_per_batch: Optional[int] = None,
|
|
) -> dict:
|
|
payload: dict[str, Any] = {"run_in_background": run_in_background}
|
|
if datasets:
|
|
payload["datasets"] = datasets
|
|
if chunks_per_batch is not None:
|
|
payload["chunks_per_batch"] = chunks_per_batch
|
|
r = self._get_client().post(self._url("/api/v1/cognify"), json=payload)
|
|
self._raise_for_status(r)
|
|
return r.json()
|
|
|
|
# -- search ----------------------------------------------------------
|
|
|
|
def search(
|
|
self,
|
|
query: str,
|
|
search_type: str = "GRAPH_COMPLETION",
|
|
datasets: Optional[list[str]] = None,
|
|
top_k: int = 15,
|
|
) -> list:
|
|
payload: dict[str, Any] = {
|
|
"query": query,
|
|
"search_type": search_type,
|
|
"top_k": top_k,
|
|
}
|
|
if datasets:
|
|
payload["datasets"] = datasets
|
|
r = self._get_client().post(self._url("/api/v1/search"), json=payload)
|
|
self._raise_for_status(r)
|
|
return r.json()
|
|
|
|
# -- memify ----------------------------------------------------------
|
|
|
|
def memify(
|
|
self,
|
|
dataset_name: Optional[str] = None,
|
|
dataset_id: Optional[str] = None,
|
|
data: Optional[str] = None,
|
|
node_name: Optional[list[str]] = None,
|
|
run_in_background: bool = False,
|
|
) -> dict:
|
|
payload: dict[str, Any] = {"run_in_background": run_in_background}
|
|
if dataset_name:
|
|
payload["dataset_name"] = dataset_name
|
|
if dataset_id:
|
|
payload["dataset_id"] = dataset_id
|
|
if data:
|
|
payload["data"] = data
|
|
if node_name:
|
|
payload["node_name"] = node_name
|
|
r = self._get_client().post(self._url("/api/v1/memify"), json=payload)
|
|
self._raise_for_status(r)
|
|
return r.json()
|
|
|
|
# -- datasets --------------------------------------------------------
|
|
|
|
def datasets_list(self) -> list[dict]:
|
|
r = self._get_client().get(self._url("/api/v1/datasets"))
|
|
self._raise_for_status(r)
|
|
return r.json()
|
|
|
|
def datasets_create(self, name: str) -> dict:
|
|
r = self._get_client().post(self._url("/api/v1/datasets"), json={"name": name})
|
|
self._raise_for_status(r)
|
|
return r.json()
|
|
|
|
def datasets_data(self, dataset_id: str) -> list[dict]:
|
|
r = self._get_client().get(self._url(f"/api/v1/datasets/{dataset_id}/data"))
|
|
self._raise_for_status(r)
|
|
return r.json()
|
|
|
|
def datasets_status(
|
|
self, dataset_ids: list[str], pipelines: Optional[list[str]] = None
|
|
) -> dict:
|
|
params = [("dataset", did) for did in dataset_ids]
|
|
if pipelines:
|
|
params.extend([("pipeline", pipeline) for pipeline in pipelines])
|
|
r = self._get_client().get(self._url("/api/v1/datasets/status"), params=params)
|
|
self._raise_for_status(r)
|
|
return r.json()
|
|
|
|
def datasets_graph(self, dataset_id: str) -> dict:
|
|
r = self._get_client().get(self._url(f"/api/v1/datasets/{dataset_id}/graph"))
|
|
self._raise_for_status(r)
|
|
return r.json()
|
|
|
|
def datasets_delete(self, dataset_id: str) -> None:
|
|
r = self._get_client().delete(self._url(f"/api/v1/datasets/{dataset_id}"))
|
|
self._raise_for_status(r)
|
|
|
|
def datasets_delete_all(self) -> None:
|
|
r = self._get_client().delete(self._url("/api/v1/datasets"))
|
|
self._raise_for_status(r)
|
|
|
|
# -- remember --------------------------------------------------------
|
|
|
|
def remember(
|
|
self,
|
|
data_items: list[str],
|
|
dataset_name: str = "main_dataset",
|
|
session_id: Optional[str] = None,
|
|
node_set: Optional[list[str]] = None,
|
|
run_in_background: bool = False,
|
|
chunk_size: Optional[int] = None,
|
|
chunks_per_batch: Optional[int] = None,
|
|
custom_prompt: Optional[str] = None,
|
|
) -> dict:
|
|
files = []
|
|
opened = []
|
|
try:
|
|
for item in data_items:
|
|
if os.path.isfile(item):
|
|
mime, _ = mimetypes.guess_type(item)
|
|
fh = open(item, "rb") # noqa: SIM115
|
|
opened.append(fh)
|
|
files.append(
|
|
("data", (os.path.basename(item), fh, mime or "application/octet-stream"))
|
|
)
|
|
else:
|
|
files.append(
|
|
(
|
|
"data",
|
|
(f"text_{len(files)}.txt", io.BytesIO(item.encode()), "text/plain"),
|
|
)
|
|
)
|
|
form_data: dict[str, Any] = {
|
|
"datasetName": dataset_name,
|
|
"run_in_background": "true" if run_in_background else "false",
|
|
}
|
|
if session_id:
|
|
form_data["session_id"] = session_id
|
|
if chunk_size is not None:
|
|
form_data["chunk_size"] = str(chunk_size)
|
|
if chunks_per_batch is not None:
|
|
form_data["chunks_per_batch"] = str(chunks_per_batch)
|
|
if custom_prompt:
|
|
form_data["custom_prompt"] = custom_prompt
|
|
if node_set:
|
|
form_data["node_set"] = node_set
|
|
r = self._get_client().post(
|
|
self._url("/api/v1/remember"),
|
|
files=files,
|
|
data=form_data,
|
|
)
|
|
self._raise_for_status(r)
|
|
return r.json()
|
|
finally:
|
|
for fh in opened:
|
|
fh.close()
|
|
|
|
# -- recall ----------------------------------------------------------
|
|
|
|
def recall(
|
|
self,
|
|
query: str,
|
|
search_type: Optional[str] = "GRAPH_COMPLETION",
|
|
datasets: Optional[list[str]] = None,
|
|
top_k: int = 15,
|
|
system_prompt: Optional[str] = None,
|
|
session_id: Optional[str] = None,
|
|
node_name: Optional[list[str]] = None,
|
|
only_context: bool = False,
|
|
verbose: bool = False,
|
|
) -> list:
|
|
# search_type=None opts the server into auto-routing (session-only
|
|
# mode when session_id is set, graph otherwise).
|
|
payload: dict[str, Any] = {
|
|
"query": query,
|
|
"search_type": search_type,
|
|
"top_k": top_k,
|
|
"only_context": only_context,
|
|
"verbose": verbose,
|
|
}
|
|
if datasets:
|
|
payload["datasets"] = datasets
|
|
if system_prompt:
|
|
payload["system_prompt"] = system_prompt
|
|
if session_id:
|
|
payload["session_id"] = session_id
|
|
if node_name:
|
|
payload["node_name"] = node_name
|
|
r = self._get_client().post(self._url("/api/v1/recall"), json=payload)
|
|
self._raise_for_status(r)
|
|
return r.json()
|
|
|
|
# -- improve ---------------------------------------------------------
|
|
|
|
def improve(
|
|
self,
|
|
dataset_name: Optional[str] = None,
|
|
dataset_id: Optional[str] = None,
|
|
node_name: Optional[list[str]] = None,
|
|
session_ids: Optional[list[str]] = None,
|
|
run_in_background: bool = False,
|
|
) -> dict:
|
|
payload: dict[str, Any] = {"run_in_background": run_in_background}
|
|
if dataset_name:
|
|
payload["dataset_name"] = dataset_name
|
|
if dataset_id:
|
|
payload["dataset_id"] = str(dataset_id)
|
|
if node_name:
|
|
payload["node_name"] = node_name
|
|
if session_ids:
|
|
payload["session_ids"] = session_ids
|
|
r = self._get_client().post(self._url("/api/v1/improve"), json=payload)
|
|
self._raise_for_status(r)
|
|
return r.json()
|
|
|
|
# -- forget ----------------------------------------------------------
|
|
|
|
def forget(
|
|
self,
|
|
dataset: Optional[str] = None,
|
|
dataset_id: Optional[str] = None,
|
|
data_id: Optional[str] = None,
|
|
everything: bool = False,
|
|
memory_only: bool = False,
|
|
) -> dict:
|
|
payload: dict[str, Any] = {
|
|
"everything": everything,
|
|
"memory_only": memory_only,
|
|
}
|
|
if dataset is not None:
|
|
payload["dataset"] = str(dataset)
|
|
if dataset_id is not None:
|
|
payload["dataset_id"] = str(dataset_id)
|
|
if data_id is not None:
|
|
payload["data_id"] = str(data_id)
|
|
r = self._get_client().post(self._url("/api/v1/forget"), json=payload)
|
|
self._raise_for_status(r)
|
|
return r.json()
|