Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
eb6a34d
chore: files changed vendor/tinycortex
senamakel Aug 20, 2026
54fb4a0
fix(remote): handle missing graph data in cognee_graph adapter
senamakel Aug 20, 2026
118f355
feat(remote): add mem0 graph adapter
senamakel Aug 20, 2026
3fe0dc4
fix(remote): handle missing graph provider file
senamakel Aug 20, 2026
cdce6ff
fix(remote): handle missing graph provider in remote adapter
senamakel Aug 20, 2026
931e92d
chore(remote): remove unused import of `std::sync::Arc`
senamakel Aug 20, 2026
841a76b
fix(remote): handle connection timeout during adapter initialization
senamakel Aug 20, 2026
d8f87bf
chore(deps): update tinycortex subproject commit
senamakel Aug 20, 2026
46a59f5
feat(tinymemory-testing-ui): add initial project scaffolding
senamakel Aug 20, 2026
25dd722
chore(tinymemory-testing-ui): add Cargo.toml for new testing UI crate
senamakel Aug 20, 2026
b3eb5be
fix(ui): correct memory leak in testing UI by ensuring proper cleanup
senamakel Aug 20, 2026
36fc4bc
chore(workspace): add tinymemory-testing-ui as a workspace member
senamakel Aug 20, 2026
a13ca9c
chore(deps): update Cargo.lock for new dependencies
senamakel Aug 20, 2026
a0757fb
Merge remote-tracking branch 'refs/remotes/upstream/main' into pr/70
senamakel Aug 20, 2026
570faa1
feat(remote): add API-key-based cloud authentication for Cognee and Mem0
senamakel Aug 20, 2026
4b448a3
feat(ui): add deployment selector and improve form accessibility
senamakel Aug 20, 2026
6a7af00
feat(ui): add deployment mode selector for remote engines
senamakel Aug 20, 2026
733e785
docs(tinymemory-testing-ui): clarify hosted API caveat and credential…
senamakel Aug 20, 2026
4224a37
test(remote): add graph auth test for cloud and self-hosted API keys
senamakel Aug 20, 2026
0a53546
feat(ui): add explicit deployment type validation and improve UI styling
senamakel Aug 20, 2026
707d5a5
chore(remote): reformat test assertions for consistent style
senamakel Aug 20, 2026
1dbe8d0
feat(remote): add retry logic for transient HTTP failures
senamakel Aug 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
[workspace]
# `sync` is the engine-neutral Composio normalisers (issue #18 §B3).
members = [".", "api", "core", "sync", "sources", "adapters/tinycortex", "adapters/remote", "conformance"]
members = [".", "api", "core", "sync", "sources", "adapters/tinycortex", "adapters/remote", "conformance", "crates/tinymemory-testing-ui"]
default-members = [".", "api", "core", "sync", "sources", "adapters/tinycortex", "adapters/remote", "conformance"]
# `crates/tinymemory-testing-ui` is deliberately left out of `default-members`:
# it is a manual testing harness, not part of the crate's build/release
# surface, so the four contract commands (which omit `-p`/`--workspace`) never
# touch it. Build or run it explicitly with `-p tinymemory-testing-ui`. Unlike
# `crates/tinymemory-module` it is a normal member here, not its own workspace
# root: it needs the root's `[patch.crates-io]` table to resolve `tinycortex`,
# and it carries none of the tinybus-inheritance problem documented above.
# `vendor/` holds engine submodules (tinycortex, tinybus, tinyagents), each of
# which is its own workspace with its own lockfile. Same exclusion
# `vendor/tinycortex` uses for its own nested vendor directory.
Expand Down
197 changes: 197 additions & 0 deletions adapters/remote/src/cognee_graph.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
//! [`CogneeGraph`] — a read-only [`MemoryGraph`] over Cognee's derived
//! knowledge graph.
//!
//! Cognee's graph is **built by its `cognify` pipeline** over ingested
//! documents, not a generic key/value store with hand-editable relations:
//! there is no endpoint to write an arbitrary KV record, and no endpoint to
//! insert a graph edge directly. So this implements exactly the one method
//! that has a genuine Cognee counterpart —
//! `relations`, backed by `GET /api/v1/datasets/{dataset_id}/graph` — and
//! returns [`MemoryError::Other`] for every method that has none (`kv_get`,
//! `kv_put`, `kv_delete`, `kv_list`, `put_relation`), rather than faking
//! empty success.

use anyhow::anyhow;
use async_trait::async_trait;
use reqwest::Method;
use serde_json::Value;
use tinymemory_api::error::MemoryError;
use tinymemory_api::provider::MemoryGraph;
use tinymemory_api::types::{GraphRelationRecord, MemoryKvRecord};

use crate::common::{stable_id, Attempts, HttpClient};

/// Read-only relation queries over one Cognee dataset's knowledge graph.
#[derive(Debug)]
pub struct CogneeGraph {
client: HttpClient,
}

impl CogneeGraph {
/// Connect to the same self-hosted Cognee server a [`crate::CogneeMemory`]
/// targets (`::new`/`::self_hosted`).
///
/// # Errors
///
/// Returns an error when `endpoint` is not an HTTP(S) URL.
pub fn new(endpoint: &str, access_token: Option<&str>) -> anyhow::Result<Self> {
Ok(Self {
client: HttpClient::bearer(endpoint, access_token)?,
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/// Connect to a Cognee Cloud tenant using `X-Api-Key` authentication.
///
/// # Errors
///
/// Returns an error when `endpoint` is invalid or `api_key` is blank.
pub fn api(endpoint: &str, api_key: &str) -> anyhow::Result<Self> {
anyhow::ensure!(
!api_key.trim().is_empty(),
"cognee API key must not be empty"
);
Ok(Self {
client: HttpClient::api_key(endpoint, Some(api_key))?,
})
}

/// Matches [`crate::cognee`]'s private `CogneeDialect::dataset_name`
/// exactly, so both halves resolve one TinyMemory namespace to the same
/// Cognee dataset.
fn dataset_name(namespace: &str) -> String {
format!("tinymemory__{}", stable_id("dataset", namespace))
}

async fn find_dataset_id(&self, namespace: &str) -> anyhow::Result<Option<String>> {
let name = Self::dataset_name(namespace);
let response: Value = self
.client
.json(
Method::GET,
"api/v1/datasets/",
None,
Attempts::RetryTransient,
)
.await?;
Ok(response
.as_array()
.into_iter()
.flatten()
.find(|value| value.get("name").and_then(Value::as_str) == Some(name.as_str()))
.and_then(|value| value.get("id").and_then(Value::as_str))
.map(str::to_owned))
}
}

const NO_KV_STORE: &str = "cognee has no generic key/value store to read or write";
const NO_WRITABLE_GRAPH: &str =
"cognee's graph is derived by the cognify pipeline over ingested documents and cannot be edited directly";

#[async_trait]
impl MemoryGraph for CogneeGraph {
async fn kv_get(
&self,
_namespace: Option<&str>,
_key: &str,
) -> Result<Option<MemoryKvRecord>, MemoryError> {
Err(MemoryError::Other(anyhow!(NO_KV_STORE)))
}

async fn kv_put(
&self,
_namespace: Option<&str>,
_key: &str,
_value: serde_json::Value,
) -> Result<(), MemoryError> {
Err(MemoryError::Other(anyhow!(NO_KV_STORE)))
}

async fn kv_delete(&self, _namespace: Option<&str>, _key: &str) -> Result<bool, MemoryError> {
Err(MemoryError::Other(anyhow!(NO_KV_STORE)))
}

async fn kv_list(
&self,
_namespace: Option<&str>,
_prefix: Option<&str>,
_limit: usize,
) -> Result<Vec<MemoryKvRecord>, MemoryError> {
Err(MemoryError::Other(anyhow!(NO_KV_STORE)))
}

/// Reads the dataset's derived graph and reshapes it into
/// `(subject, predicate, object)` triples.
///
/// Cognee's graph endpoint takes only a dataset id, not a subject or
/// predicate filter, so this fetches the whole dataset graph and filters
/// client-side. `namespace: None` ("the global, namespace-less slice") has
/// no Cognee counterpart — every dataset is namespace-scoped — so it is
/// rejected as invalid input rather than silently returning nothing.
async fn relations(
&self,
namespace: Option<&str>,
subject: Option<&str>,
predicate: Option<&str>,
limit: usize,
) -> Result<Vec<GraphRelationRecord>, MemoryError> {
let namespace = namespace.ok_or_else(|| {
MemoryError::Invalid(
"cognee requires a namespace to resolve a dataset graph".to_string(),
)
})?;
let Some(dataset_id) = self.find_dataset_id(namespace).await? else {
return Ok(Vec::new());
};
let graph: Value = self
.client
.json(
Method::GET,
&format!("api/v1/datasets/{dataset_id}/graph"),
None,
Attempts::RetryTransient,
)
.await?;
let nodes = graph.get("nodes").and_then(Value::as_array);
let labels: std::collections::HashMap<&str, &str> = nodes
.into_iter()
.flatten()
.filter_map(|node| {
Some((
node.get("id")?.as_str()?,
node.get("label")?.as_str().unwrap_or_default(),
))
})
.collect();

let edges = graph.get("edges").and_then(Value::as_array);
let relations = edges
.into_iter()
.flatten()
.filter_map(|edge| {
let source = edge.get("source")?.as_str()?;
let target = edge.get("target")?.as_str()?;
let label = edge.get("label")?.as_str().unwrap_or_default();
Some(GraphRelationRecord {
namespace: Some(namespace.to_string()),
subject: labels.get(source).copied().unwrap_or(source).to_string(),
predicate: label.to_string(),
object: labels.get(target).copied().unwrap_or(target).to_string(),
attrs: Value::Null,
updated_at: 0.0,
evidence_count: 1,
order_index: None,
document_ids: Vec::new(),
chunk_ids: Vec::new(),
})
})
.filter(|relation| subject.is_none_or(|s| relation.subject == s))
.filter(|relation| predicate.is_none_or(|p| relation.predicate == p))
.take(limit)
.collect();
Ok(relations)
}

async fn put_relation(&self, _relation: GraphRelationRecord) -> Result<(), MemoryError> {
Err(MemoryError::Other(anyhow!(NO_WRITABLE_GRAPH)))
}
}
54 changes: 53 additions & 1 deletion adapters/remote/src/cognee_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use axum::{
};
use serde_json::{json, Value};
use tinymemory_api::{
provider::{MemoryCore, MemoryProvider, MemoryRecall},
provider::{MemoryCore, MemoryGraph, MemoryProvider, MemoryRecall},
recall::OwnedRecallOpts,
traits::Memory,
types::{MemoryCategory, MemoryTaint},
Expand Down Expand Up @@ -109,6 +109,21 @@ async fn capture_auth(State(state): State<Arc<Mutex<Value>>>, headers: HeaderMap
StatusCode::OK
}

async fn capture_graph_auth(
State(state): State<Arc<Mutex<Value>>>,
headers: HeaderMap,
) -> Json<Value> {
*state.lock().expect("state lock") = json!({
"authorization": headers
.get("authorization")
.and_then(|value| value.to_str().ok()),
"api_key": headers
.get("x-api-key")
.and_then(|value| value.to_str().ok()),
});
Json(Value::Array(Vec::new()))
}

#[tokio::test]
async fn cognee_supports_cloud_api_keys_and_self_hosted_bearer_tokens() {
let captured = Arc::new(Mutex::new(Value::Null));
Expand Down Expand Up @@ -141,6 +156,43 @@ async fn cognee_supports_cloud_api_keys_and_self_hosted_bearer_tokens() {
assert!(super::CogneeMemory::api(&endpoint, " ").is_err());
}

#[tokio::test]
async fn cognee_graph_supports_cloud_api_keys_and_self_hosted_bearer_tokens() {
let captured = Arc::new(Mutex::new(Value::Null));
let app = Router::new()
.route("/api/v1/datasets/", get(capture_graph_auth))
.with_state(captured.clone());
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind");
let endpoint = format!("http://{}", listener.local_addr().expect("address"));
tokio::spawn(async move {
axum::serve(listener, app).await.expect("serve");
});

let api = crate::CogneeGraph::api(&endpoint, "cloud-secret").expect("api graph client");
assert!(api
.relations(Some("project"), None, None, 10)
.await
.expect("cloud relations")
.is_empty());
let api_headers = captured.lock().expect("state lock").clone();
assert_eq!(api_headers["api_key"], "cloud-secret");
assert!(api_headers["authorization"].is_null());

let hosted =
crate::CogneeGraph::new(&endpoint, Some("local-secret")).expect("self-hosted graph client");
assert!(hosted
.relations(Some("project"), None, None, 10)
.await
.expect("self-hosted relations")
.is_empty());
let hosted_headers = captured.lock().expect("state lock").clone();
assert_eq!(hosted_headers["authorization"], "Bearer local-secret");
assert!(hosted_headers["api_key"].is_null());
assert!(crate::CogneeGraph::api(&endpoint, " ").is_err());
}

#[test]
fn cognee_remote_names_are_bounded_and_safe_for_arbitrary_contract_keys() {
let unusual = format!("tenant / 🧠 / {}", "x".repeat(500));
Expand Down
Loading
Loading