-
Notifications
You must be signed in to change notification settings - Fork 4
Add a local testing UI, and Cognee/Mem0 graph support #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 54fb4a0
fix(remote): handle missing graph data in cognee_graph adapter
senamakel 118f355
feat(remote): add mem0 graph adapter
senamakel 3fe0dc4
fix(remote): handle missing graph provider file
senamakel cdce6ff
fix(remote): handle missing graph provider in remote adapter
senamakel 931e92d
chore(remote): remove unused import of `std::sync::Arc`
senamakel 841a76b
fix(remote): handle connection timeout during adapter initialization
senamakel d8f87bf
chore(deps): update tinycortex subproject commit
senamakel 46a59f5
feat(tinymemory-testing-ui): add initial project scaffolding
senamakel 25dd722
chore(tinymemory-testing-ui): add Cargo.toml for new testing UI crate
senamakel b3eb5be
fix(ui): correct memory leak in testing UI by ensuring proper cleanup
senamakel 36fc4bc
chore(workspace): add tinymemory-testing-ui as a workspace member
senamakel a13ca9c
chore(deps): update Cargo.lock for new dependencies
senamakel a0757fb
Merge remote-tracking branch 'refs/remotes/upstream/main' into pr/70
senamakel 570faa1
feat(remote): add API-key-based cloud authentication for Cognee and Mem0
senamakel 4b448a3
feat(ui): add deployment selector and improve form accessibility
senamakel 6a7af00
feat(ui): add deployment mode selector for remote engines
senamakel 733e785
docs(tinymemory-testing-ui): clarify hosted API caveat and credential…
senamakel 4224a37
test(remote): add graph auth test for cloud and self-hosted API keys
senamakel 0a53546
feat(ui): add explicit deployment type validation and improve UI styling
senamakel 707d5a5
chore(remote): reformat test assertions for consistent style
senamakel 1dbe8d0
feat(remote): add retry logic for transient HTTP failures
senamakel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)?, | ||
| }) | ||
| } | ||
|
|
||
| /// 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))) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.