From e73243f49661c29149ec5d42b1dfc716c9dfead9 Mon Sep 17 00:00:00 2001 From: Boris Arzentar Date: Fri, 25 Oct 2024 10:08:11 +0200 Subject: [PATCH 1/5] feat: log search queries and results --- .../src/modules/chat/getHistory.ts | 8 +++++ .../src/ui/Partials/SearchView/SearchView.tsx | 15 +++++++-- cognee/__init__.py | 2 +- cognee/api/client.py | 19 ++++++++++++ cognee/api/v1/search/__init__.py | 1 + cognee/api/v1/search/get_search_history.py | 9 ++++++ cognee/api/v1/search/search_v2.py | 16 +++++++--- cognee/modules/search/models/Query.py | 16 ++++++++++ cognee/modules/search/models/Result.py | 16 ++++++++++ cognee/modules/search/operations/__init__.py | 3 ++ .../modules/search/operations/get_history.py | 31 +++++++++++++++++++ .../modules/search/operations/get_queries.py | 17 ++++++++++ .../modules/search/operations/get_results.py | 17 ++++++++++ cognee/modules/search/operations/log_query.py | 19 ++++++++++++ .../modules/search/operations/log_result.py | 15 +++++++++ cognee/tests/test_library.py | 9 ++++-- cognee/tests/test_neo4j.py | 9 ++++-- cognee/tests/test_pgvector.py | 9 ++++-- cognee/tests/test_qdrant.py | 9 ++++-- cognee/tests/test_weaviate.py | 9 ++++-- notebooks/cognee_demo.ipynb | 6 ++-- 21 files changed, 229 insertions(+), 26 deletions(-) create mode 100644 cognee-frontend/src/modules/chat/getHistory.ts create mode 100644 cognee/api/v1/search/get_search_history.py create mode 100644 cognee/modules/search/models/Query.py create mode 100644 cognee/modules/search/models/Result.py create mode 100644 cognee/modules/search/operations/__init__.py create mode 100644 cognee/modules/search/operations/get_history.py create mode 100644 cognee/modules/search/operations/get_queries.py create mode 100644 cognee/modules/search/operations/get_results.py create mode 100644 cognee/modules/search/operations/log_query.py create mode 100644 cognee/modules/search/operations/log_result.py diff --git a/cognee-frontend/src/modules/chat/getHistory.ts b/cognee-frontend/src/modules/chat/getHistory.ts new file mode 100644 index 00000000..dce914da --- /dev/null +++ b/cognee-frontend/src/modules/chat/getHistory.ts @@ -0,0 +1,8 @@ +import { fetch } from '@/utils'; + +export default function getHistory() { + return fetch( + '/v1/search', + ) + .then((response) => response.json()); +} diff --git a/cognee-frontend/src/ui/Partials/SearchView/SearchView.tsx b/cognee-frontend/src/ui/Partials/SearchView/SearchView.tsx index b20beb5b..b4fa0777 100644 --- a/cognee-frontend/src/ui/Partials/SearchView/SearchView.tsx +++ b/cognee-frontend/src/ui/Partials/SearchView/SearchView.tsx @@ -1,9 +1,12 @@ +'use client'; + import { v4 } from 'uuid'; import classNames from 'classnames'; -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { CTAButton, Stack, Text, DropdownSelect, TextArea, useBoolean } from 'ohmy-ui'; import { fetch } from '@/utils'; import styles from './SearchView.module.css'; +import getHistory from '@/modules/chat/getHistory'; interface Message { id: string; @@ -52,6 +55,14 @@ export default function SearchView() { }, 300); }, []); + useEffect(() => { + getHistory() + .then((history) => { + setMessages(history); + scrollToBottom(); + }); + }, [scrollToBottom]); + const handleSearchSubmit = useCallback((event: React.FormEvent) => { event.preventDefault(); @@ -78,7 +89,7 @@ export default function SearchView() { 'Content-Type': 'application/json', }, body: JSON.stringify({ - query: inputValue, + query: inputValue.trim(), searchType: searchTypeValue, }), }) diff --git a/cognee/__init__.py b/cognee/__init__.py index aca7f5d4..e89ef1dc 100644 --- a/cognee/__init__.py +++ b/cognee/__init__.py @@ -2,7 +2,7 @@ from .api.v1.add import add from .api.v1.cognify import cognify from .api.v1.datasets.datasets import datasets -from .api.v1.search import search, SearchType +from .api.v1.search import search, SearchType, get_search_history from .api.v1.prune import prune # Pipelines diff --git a/cognee/api/client.py b/cognee/api/client.py index 4b41f057..a229fa2b 100644 --- a/cognee/api/client.py +++ b/cognee/api/client.py @@ -15,6 +15,7 @@ from cognee.api.DTO import InDTO, OutDTO from cognee.api.v1.search import SearchType +from cognee.modules.search.operations import get_history from cognee.modules.users.models import User from cognee.modules.users.methods import get_authenticated_user from cognee.modules.pipelines.models import PipelineRunStatus @@ -350,6 +351,24 @@ async def search(payload: SearchPayloadDTO, user: User = Depends(get_authenticat content = {"error": str(error)} ) +class SearchHistoryItem(OutDTO): + id: UUID + text: str + user: str + created_at: datetime + +@app.get("/api/v1/search", response_model = list[SearchHistoryItem]) +async def get_search_history(user: User = Depends(get_authenticated_user)): + try: + history = await get_history(user.id) + + return history + except Exception as error: + return JSONResponse( + status_code = 500, + content = {"error": str(error)} + ) + from cognee.modules.settings.get_settings import LLMConfig, VectorDBConfig class LLMConfigDTO(OutDTO, LLMConfig): diff --git a/cognee/api/v1/search/__init__.py b/cognee/api/v1/search/__init__.py index f01dcd63..91cf35c8 100644 --- a/cognee/api/v1/search/__init__.py +++ b/cognee/api/v1/search/__init__.py @@ -1 +1,2 @@ from .search_v2 import search, SearchType +from .get_search_history import get_search_history diff --git a/cognee/api/v1/search/get_search_history.py b/cognee/api/v1/search/get_search_history.py new file mode 100644 index 00000000..fada67c8 --- /dev/null +++ b/cognee/api/v1/search/get_search_history.py @@ -0,0 +1,9 @@ +from cognee.modules.search.operations import get_history +from cognee.modules.users.methods import get_default_user +from cognee.modules.users.models import User + +async def get_search_history(user: User = None) -> list: + if not user: + user = await get_default_user() + + return await get_history(user.id) diff --git a/cognee/api/v1/search/search_v2.py b/cognee/api/v1/search/search_v2.py index b3d45d71..5aa8b4cf 100644 --- a/cognee/api/v1/search/search_v2.py +++ b/cognee/api/v1/search/search_v2.py @@ -1,6 +1,8 @@ +import json from uuid import UUID from enum import Enum from typing import Callable, Dict +from cognee.modules.search.operations import log_query, log_result from cognee.shared.utils import send_telemetry from cognee.modules.users.models import User from cognee.modules.users.methods import get_default_user @@ -14,15 +16,17 @@ class SearchType(Enum): INSIGHTS = "INSIGHTS" CHUNKS = "CHUNKS" -async def search(search_type: SearchType, query: str, user: User = None) -> list: +async def search(query_type: SearchType, query_text: str, user: User = None) -> list: if user is None: user = await get_default_user() if user is None: raise PermissionError("No user found in the system. Please create a user.") + query = await log_query(query_text, str(query_type), user.id) + own_document_ids = await get_document_ids_for_user(user.id) - search_results = await specific_search(search_type, query, user) + search_results = await specific_search(query_type, query_text, user) filtered_search_results = [] @@ -33,19 +37,21 @@ async def search(search_type: SearchType, query: str, user: User = None) -> list if document_id is None or document_id in own_document_ids: filtered_search_results.append(search_result) + await log_result(query.id, json.dumps(filtered_search_results), user.id) + return filtered_search_results -async def specific_search(search_type: SearchType, query: str, user) -> list: +async def specific_search(query_type: SearchType, query: str, user) -> list: search_tasks: Dict[SearchType, Callable] = { SearchType.SUMMARIES: query_summaries, SearchType.INSIGHTS: query_graph_connections, SearchType.CHUNKS: query_chunks, } - search_task = search_tasks.get(search_type) + search_task = search_tasks.get(query_type) if search_task is None: - raise ValueError(f"Unsupported search type: {search_type}") + raise ValueError(f"Unsupported search type: {query_type}") send_telemetry("cognee.search EXECUTION STARTED", user.id) diff --git a/cognee/modules/search/models/Query.py b/cognee/modules/search/models/Query.py new file mode 100644 index 00000000..f043dfd1 --- /dev/null +++ b/cognee/modules/search/models/Query.py @@ -0,0 +1,16 @@ +from uuid import uuid4 +from datetime import datetime, timezone +from sqlalchemy import Column, DateTime, String +from cognee.infrastructure.databases.relational import Base, UUID + +class Query(Base): + __tablename__ = "queries" + + id = Column(UUID, primary_key = True, default = uuid4) + + text = Column(String) + query_type = Column(String) + user_id = Column(UUID) + + created_at = Column(DateTime(timezone = True), default = lambda: datetime.now(timezone.utc)) + updated_at = Column(DateTime(timezone = True), onupdate = lambda: datetime.now(timezone.utc)) diff --git a/cognee/modules/search/models/Result.py b/cognee/modules/search/models/Result.py new file mode 100644 index 00000000..18366d24 --- /dev/null +++ b/cognee/modules/search/models/Result.py @@ -0,0 +1,16 @@ +from datetime import datetime, timezone +from uuid import uuid4 +from sqlalchemy import Column, DateTime, Text +from cognee.infrastructure.databases.relational import Base, UUID + +class Result(Base): + __tablename__ = "results" + + id = Column(UUID, primary_key = True, default = uuid4) + + value = Column(Text) + query_id = Column(UUID) + user_id = Column(UUID) + + created_at = Column(DateTime(timezone = True), default = lambda: datetime.now(timezone.utc)) + updated_at = Column(DateTime(timezone = True), onupdate = lambda: datetime.now(timezone.utc)) diff --git a/cognee/modules/search/operations/__init__.py b/cognee/modules/search/operations/__init__.py new file mode 100644 index 00000000..41d2a4e4 --- /dev/null +++ b/cognee/modules/search/operations/__init__.py @@ -0,0 +1,3 @@ +from .log_query import log_query +from .log_result import log_result +from .get_history import get_history diff --git a/cognee/modules/search/operations/get_history.py b/cognee/modules/search/operations/get_history.py new file mode 100644 index 00000000..831c4acc --- /dev/null +++ b/cognee/modules/search/operations/get_history.py @@ -0,0 +1,31 @@ +from uuid import UUID +from sqlalchemy import literal, select +from cognee.infrastructure.databases.relational import get_relational_engine +from ..models.Query import Query +from ..models.Result import Result + +async def get_history(user_id: UUID, limit: int = 10) -> list[Result]: + db_engine = get_relational_engine() + + queries_query = select( + Query.id, + Query.text.label("text"), + Query.created_at, + literal("user").label("user") + ) \ + .filter(Query.user_id == user_id) + + results_query = select( + Result.id, + Result.value.label("text"), + Result.created_at, + literal("system").label("user") + ) \ + .filter(Result.user_id == user_id) + + history_query = queries_query.union(results_query).order_by("created_at").limit(limit) + + async with db_engine.get_async_session() as session: + history = (await session.execute(history_query)).all() + + return history diff --git a/cognee/modules/search/operations/get_queries.py b/cognee/modules/search/operations/get_queries.py new file mode 100644 index 00000000..fb7b56b3 --- /dev/null +++ b/cognee/modules/search/operations/get_queries.py @@ -0,0 +1,17 @@ +from uuid import UUID +from sqlalchemy import select +from cognee.infrastructure.databases.relational import get_relational_engine +from ..models.Query import Query + +async def get_queries(user_id: UUID, limit: int) -> list[Query]: + db_engine = get_relational_engine() + + async with db_engine.get_async_session() as session: + datasets = (await session.scalars( + select(Query) + .filter(Query.user_id == user_id) + .order_by(Query.created_at.desc()) + .limit(limit) + )).all() + + return datasets diff --git a/cognee/modules/search/operations/get_results.py b/cognee/modules/search/operations/get_results.py new file mode 100644 index 00000000..3420a606 --- /dev/null +++ b/cognee/modules/search/operations/get_results.py @@ -0,0 +1,17 @@ +from uuid import UUID +from sqlalchemy import select +from cognee.infrastructure.databases.relational import get_relational_engine +from ..models.Result import Result + +async def get_results(user_id: UUID, limit: int = 10) -> list[Result]: + db_engine = get_relational_engine() + + async with db_engine.get_async_session() as session: + datasets = (await session.scalars( + select(Result) + .filter(Result.user_id == user_id) + .order_by(Result.created_at.desc()) + .limit(limit) + )).all() + + return datasets diff --git a/cognee/modules/search/operations/log_query.py b/cognee/modules/search/operations/log_query.py new file mode 100644 index 00000000..02ed3f15 --- /dev/null +++ b/cognee/modules/search/operations/log_query.py @@ -0,0 +1,19 @@ +from uuid import UUID +from cognee.infrastructure.databases.relational import get_relational_engine +from ..models.Query import Query + +async def log_query(query_text: str, query_type: str, user_id: UUID) -> Query: + db_engine = get_relational_engine() + + async with db_engine.get_async_session() as session: + query = Query( + text = query_text, + query_type = query_type, + user_id = user_id, + ) + + session.add(query) + + await session.commit() + + return query diff --git a/cognee/modules/search/operations/log_result.py b/cognee/modules/search/operations/log_result.py new file mode 100644 index 00000000..b81e0b44 --- /dev/null +++ b/cognee/modules/search/operations/log_result.py @@ -0,0 +1,15 @@ +from uuid import UUID +from cognee.infrastructure.databases.relational import get_relational_engine +from ..models.Result import Result + +async def log_result(query_id: UUID, result: str, user_id: UUID): + db_engine = get_relational_engine() + + async with db_engine.get_async_session() as session: + session.add(Result( + value = result, + query_id = query_id, + user_id = user_id, + )) + + await session.commit() diff --git a/cognee/tests/test_library.py b/cognee/tests/test_library.py index 49533939..f8d3a26c 100755 --- a/cognee/tests/test_library.py +++ b/cognee/tests/test_library.py @@ -35,24 +35,27 @@ async def main(): random_node = (await vector_engine.search("entities", "AI"))[0] random_node_name = random_node.payload["name"] - search_results = await cognee.search(SearchType.INSIGHTS, query = random_node_name) + search_results = await cognee.search(SearchType.INSIGHTS, query_text = random_node_name) assert len(search_results) != 0, "The search results list is empty." print("\n\nExtracted sentences are:\n") for result in search_results: print(f"{result}\n") - search_results = await cognee.search(SearchType.CHUNKS, query = random_node_name) + search_results = await cognee.search(SearchType.CHUNKS, query_text = random_node_name) assert len(search_results) != 0, "The search results list is empty." print("\n\nExtracted chunks are:\n") for result in search_results: print(f"{result}\n") - search_results = await cognee.search(SearchType.SUMMARIES, query = random_node_name) + search_results = await cognee.search(SearchType.SUMMARIES, query_text = random_node_name) assert len(search_results) != 0, "Query related summaries don't exist." print("\n\Extracted summaries are:\n") for result in search_results: print(f"{result}\n") + history = await cognee.get_search_history() + + assert len(history) == 6, "Search history is not correct." if __name__ == "__main__": import asyncio diff --git a/cognee/tests/test_neo4j.py b/cognee/tests/test_neo4j.py index feff647c..79ef8c81 100644 --- a/cognee/tests/test_neo4j.py +++ b/cognee/tests/test_neo4j.py @@ -39,24 +39,27 @@ async def main(): random_node = (await vector_engine.search("entities", "AI"))[0] random_node_name = random_node.payload["name"] - search_results = await cognee.search(SearchType.INSIGHTS, query = random_node_name) + search_results = await cognee.search(SearchType.INSIGHTS, query_text = random_node_name) assert len(search_results) != 0, "The search results list is empty." print("\n\nExtracted sentences are:\n") for result in search_results: print(f"{result}\n") - search_results = await cognee.search(SearchType.CHUNKS, query = random_node_name) + search_results = await cognee.search(SearchType.CHUNKS, query_text = random_node_name) assert len(search_results) != 0, "The search results list is empty." print("\n\nExtracted chunks are:\n") for result in search_results: print(f"{result}\n") - search_results = await cognee.search(SearchType.SUMMARIES, query = random_node_name) + search_results = await cognee.search(SearchType.SUMMARIES, query_text = random_node_name) assert len(search_results) != 0, "Query related summaries don't exist." print("\n\Extracted summaries are:\n") for result in search_results: print(f"{result}\n") + history = await cognee.get_search_history() + + assert len(history) == 6, "Search history is not correct." if __name__ == "__main__": import asyncio diff --git a/cognee/tests/test_pgvector.py b/cognee/tests/test_pgvector.py index 02d292d6..47be8561 100644 --- a/cognee/tests/test_pgvector.py +++ b/cognee/tests/test_pgvector.py @@ -68,24 +68,27 @@ async def main(): random_node = (await vector_engine.search("entities", "AI"))[0] random_node_name = random_node.payload["name"] - search_results = await cognee.search(SearchType.INSIGHTS, query=random_node_name) + search_results = await cognee.search(SearchType.INSIGHTS, query_text = random_node_name) assert len(search_results) != 0, "The search results list is empty." print("\n\nExtracted sentences are:\n") for result in search_results: print(f"{result}\n") - search_results = await cognee.search(SearchType.CHUNKS, query=random_node_name) + search_results = await cognee.search(SearchType.CHUNKS, query_text = random_node_name) assert len(search_results) != 0, "The search results list is empty." print("\n\nExtracted chunks are:\n") for result in search_results: print(f"{result}\n") - search_results = await cognee.search(SearchType.SUMMARIES, query=random_node_name) + search_results = await cognee.search(SearchType.SUMMARIES, query_text = random_node_name) assert len(search_results) != 0, "Query related summaries don't exist." print("\n\nExtracted summaries are:\n") for result in search_results: print(f"{result}\n") + history = await cognee.get_search_history() + + assert len(history) == 6, "Search history is not correct." if __name__ == "__main__": import asyncio diff --git a/cognee/tests/test_qdrant.py b/cognee/tests/test_qdrant.py index 2ea011eb..7614dab9 100644 --- a/cognee/tests/test_qdrant.py +++ b/cognee/tests/test_qdrant.py @@ -40,24 +40,27 @@ async def main(): random_node = (await vector_engine.search("entities", "AI"))[0] random_node_name = random_node.payload["name"] - search_results = await cognee.search(SearchType.INSIGHTS, query = random_node_name) + search_results = await cognee.search(SearchType.INSIGHTS, query_text = random_node_name) assert len(search_results) != 0, "The search results list is empty." print("\n\nExtracted sentences are:\n") for result in search_results: print(f"{result}\n") - search_results = await cognee.search(SearchType.CHUNKS, query = random_node_name) + search_results = await cognee.search(SearchType.CHUNKS, query_text = random_node_name) assert len(search_results) != 0, "The search results list is empty." print("\n\nExtracted chunks are:\n") for result in search_results: print(f"{result}\n") - search_results = await cognee.search(SearchType.SUMMARIES, query = random_node_name) + search_results = await cognee.search(SearchType.SUMMARIES, query_text = random_node_name) assert len(search_results) != 0, "Query related summaries don't exist." print("\n\Extracted summaries are:\n") for result in search_results: print(f"{result}\n") + history = await cognee.get_search_history() + + assert len(history) == 6, "Search history is not correct." if __name__ == "__main__": import asyncio diff --git a/cognee/tests/test_weaviate.py b/cognee/tests/test_weaviate.py index 7ad29a9a..b960907a 100644 --- a/cognee/tests/test_weaviate.py +++ b/cognee/tests/test_weaviate.py @@ -38,24 +38,27 @@ async def main(): random_node = (await vector_engine.search("entities", "AI"))[0] random_node_name = random_node.payload["name"] - search_results = await cognee.search(SearchType.INSIGHTS, query = random_node_name) + search_results = await cognee.search(SearchType.INSIGHTS, query_text = random_node_name) assert len(search_results) != 0, "The search results list is empty." print("\n\nExtracted sentences are:\n") for result in search_results: print(f"{result}\n") - search_results = await cognee.search(SearchType.CHUNKS, query = random_node_name) + search_results = await cognee.search(SearchType.CHUNKS, query_text = random_node_name) assert len(search_results) != 0, "The search results list is empty." print("\n\nExtracted chunks are:\n") for result in search_results: print(f"{result}\n") - search_results = await cognee.search(SearchType.SUMMARIES, query = random_node_name) + search_results = await cognee.search(SearchType.SUMMARIES, query_text = random_node_name) assert len(search_results) != 0, "Query related summaries don't exist." print("\n\Extracted summaries are:\n") for result in search_results: print(f"{result}\n") + history = await cognee.get_search_history() + + assert len(history) == 6, "Search history is not correct." if __name__ == "__main__": import asyncio diff --git a/notebooks/cognee_demo.ipynb b/notebooks/cognee_demo.ipynb index ba5a89c8..7d7e0ea0 100644 --- a/notebooks/cognee_demo.ipynb +++ b/notebooks/cognee_demo.ipynb @@ -800,7 +800,7 @@ "node = (await vector_engine.search(\"entities\", \"sarah.nguyen@example.com\"))[0]\n", "node_name = node.payload[\"name\"]\n", "\n", - "search_results = await cognee.search(SearchType.SUMMARIES, query = node_name)\n", + "search_results = await cognee.search(SearchType.SUMMARIES, query_text = node_name)\n", "print(\"\\n\\Extracted summaries are:\\n\")\n", "for result in search_results:\n", " print(f\"{result}\\n\")" @@ -821,7 +821,7 @@ "metadata": {}, "outputs": [], "source": [ - "search_results = await cognee.search(SearchType.CHUNKS, query = node_name)\n", + "search_results = await cognee.search(SearchType.CHUNKS, query_text = node_name)\n", "print(\"\\n\\nExtracted chunks are:\\n\")\n", "for result in search_results:\n", " print(f\"{result}\\n\")" @@ -842,7 +842,7 @@ "metadata": {}, "outputs": [], "source": [ - "search_results = await cognee.search(SearchType.INSIGHTS, query = node_name)\n", + "search_results = await cognee.search(SearchType.INSIGHTS, query_text = node_name)\n", "print(\"\\n\\nExtracted sentences are:\\n\")\n", "for result in search_results:\n", " print(f\"{result}\\n\")" From a6a7b46dc73ab31dfb5cb289477ce8ce9de1a798 Mon Sep 17 00:00:00 2001 From: Boris Arzentar Date: Wed, 13 Nov 2024 15:42:59 +0100 Subject: [PATCH 2/5] fix: address coderabbit review comments --- cognee/infrastructure/pipeline/models/Operation.py | 4 ++-- cognee/modules/search/operations/get_queries.py | 4 ++-- cognee/modules/search/operations/get_results.py | 4 ++-- notebooks/cognee_demo.ipynb | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cognee/infrastructure/pipeline/models/Operation.py b/cognee/infrastructure/pipeline/models/Operation.py index 1834c1a3..62eb74c4 100644 --- a/cognee/infrastructure/pipeline/models/Operation.py +++ b/cognee/infrastructure/pipeline/models/Operation.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone from sqlalchemy.orm import Mapped, MappedColumn from sqlalchemy import Column, DateTime, ForeignKey, Enum, JSON from cognee.infrastructure.databases.relational import Base, UUID @@ -24,4 +24,4 @@ class Operation(Base): data_id = Column(UUID, ForeignKey("data.id")) meta_data: Mapped[dict] = MappedColumn(type_ = JSON) - created_at = Column(DateTime, default = datetime.utcnow) + created_at = Column(DateTime, default = datetime.now(timezone.utc)) diff --git a/cognee/modules/search/operations/get_queries.py b/cognee/modules/search/operations/get_queries.py index fb7b56b3..ded10a8e 100644 --- a/cognee/modules/search/operations/get_queries.py +++ b/cognee/modules/search/operations/get_queries.py @@ -7,11 +7,11 @@ async def get_queries(user_id: UUID, limit: int) -> list[Query]: db_engine = get_relational_engine() async with db_engine.get_async_session() as session: - datasets = (await session.scalars( + queries = (await session.scalars( select(Query) .filter(Query.user_id == user_id) .order_by(Query.created_at.desc()) .limit(limit) )).all() - return datasets + return queries diff --git a/cognee/modules/search/operations/get_results.py b/cognee/modules/search/operations/get_results.py index 3420a606..7f90a3f0 100644 --- a/cognee/modules/search/operations/get_results.py +++ b/cognee/modules/search/operations/get_results.py @@ -7,11 +7,11 @@ async def get_results(user_id: UUID, limit: int = 10) -> list[Result]: db_engine = get_relational_engine() async with db_engine.get_async_session() as session: - datasets = (await session.scalars( + results = (await session.scalars( select(Result) .filter(Result.user_id == user_id) .order_by(Result.created_at.desc()) .limit(limit) )).all() - return datasets + return results diff --git a/notebooks/cognee_demo.ipynb b/notebooks/cognee_demo.ipynb index 06cd2a86..45f5a618 100644 --- a/notebooks/cognee_demo.ipynb +++ b/notebooks/cognee_demo.ipynb @@ -791,7 +791,7 @@ "node = (await vector_engine.search(\"Entity_name\", \"sarah.nguyen@example.com\"))[0]\n", "node_name = node.payload[\"text\"]\n", "\n", - "search_results = await cognee.search(SearchType.SUMMARIES, query = node_name)\n", + "search_results = await cognee.search(SearchType.SUMMARIES, query_text = node_name)\n", "print(\"\\n\\Extracted summaries are:\\n\")\n", "for result in search_results:\n", " print(f\"{result}\\n\")" @@ -812,7 +812,7 @@ "metadata": {}, "outputs": [], "source": [ - "search_results = await cognee.search(SearchType.CHUNKS, query = node_name)\n", + "search_results = await cognee.search(SearchType.CHUNKS, query_text = node_name)\n", "print(\"\\n\\nExtracted chunks are:\\n\")\n", "for result in search_results:\n", " print(f\"{result}\\n\")" @@ -833,7 +833,7 @@ "metadata": {}, "outputs": [], "source": [ - "search_results = await cognee.search(SearchType.INSIGHTS, query = node_name)\n", + "search_results = await cognee.search(SearchType.INSIGHTS, query_text = node_name)\n", "print(\"\\n\\nExtracted sentences are:\\n\")\n", "for result in search_results:\n", " print(f\"{result}\\n\")" From 09f27e8ba4605b67d159cffac509f8ed148da729 Mon Sep 17 00:00:00 2001 From: Boris Arzentar Date: Wed, 13 Nov 2024 16:45:00 +0100 Subject: [PATCH 3/5] fix: parse UUID when logging search results --- cognee/api/v1/search/search_v2.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cognee/api/v1/search/search_v2.py b/cognee/api/v1/search/search_v2.py index abcccca6..c1bc0ee4 100644 --- a/cognee/api/v1/search/search_v2.py +++ b/cognee/api/v1/search/search_v2.py @@ -3,6 +3,7 @@ from enum import Enum from typing import Callable, Dict from cognee.modules.search.operations import log_query, log_result +from cognee.modules.storage.utils import JSONEncoder from cognee.shared.utils import send_telemetry from cognee.modules.users.models import User from cognee.modules.users.methods import get_default_user @@ -37,7 +38,7 @@ async def search(query_type: SearchType, query_text: str, user: User = None) -> if document_id is None or document_id in own_document_ids: filtered_search_results.append(search_result) - await log_result(query.id, json.dumps(filtered_search_results), user.id) + await log_result(query.id, json.dumps(filtered_search_results, cls = JSONEncoder), user.id) return filtered_search_results From 4efdeda276bf50e640a08c3cb7b06fadf702ce12 Mon Sep 17 00:00:00 2001 From: Boris Arzentar Date: Wed, 13 Nov 2024 16:46:42 +0100 Subject: [PATCH 4/5] fix: remove custom UUID type and use DB agnostic UUID from sqlalchemy --- .../databases/relational/__init__.py | 3 -- .../databases/relational/data_types/UUID.py | 45 ------------------- cognee/modules/data/models/Data.py | 4 +- cognee/modules/data/models/Dataset.py | 4 +- cognee/modules/data/models/DatasetData.py | 4 +- cognee/modules/pipelines/models/Pipeline.py | 5 ++- .../modules/pipelines/models/PipelineRun.py | 4 +- .../modules/pipelines/models/PipelineTask.py | 4 +- cognee/modules/search/models/Query.py | 4 +- cognee/modules/search/models/Result.py | 6 +-- cognee/modules/users/models/ACL.py | 4 +- cognee/modules/users/models/ACLResources.py | 4 +- cognee/modules/users/models/Group.py | 3 +- cognee/modules/users/models/Permission.py | 4 +- cognee/modules/users/models/Principal.py | 4 +- cognee/modules/users/models/Resource.py | 4 +- cognee/modules/users/models/User.py | 5 +-- cognee/modules/users/models/UserGroup.py | 4 +- 18 files changed, 33 insertions(+), 82 deletions(-) delete mode 100644 cognee/infrastructure/databases/relational/data_types/UUID.py diff --git a/cognee/infrastructure/databases/relational/__init__.py b/cognee/infrastructure/databases/relational/__init__.py index 1ef84790..09a4d669 100644 --- a/cognee/infrastructure/databases/relational/__init__.py +++ b/cognee/infrastructure/databases/relational/__init__.py @@ -2,6 +2,3 @@ from .config import get_relational_config from .create_db_and_tables import create_db_and_tables from .get_relational_engine import get_relational_engine - -# Global data types -from .data_types.UUID import UUID diff --git a/cognee/infrastructure/databases/relational/data_types/UUID.py b/cognee/infrastructure/databases/relational/data_types/UUID.py deleted file mode 100644 index 722204b3..00000000 --- a/cognee/infrastructure/databases/relational/data_types/UUID.py +++ /dev/null @@ -1,45 +0,0 @@ -import uuid - -from sqlalchemy.types import TypeDecorator, BINARY -from sqlalchemy.dialects.postgresql import UUID as psqlUUID - -class UUID(TypeDecorator): - """Platform-independent GUID type. - - Uses Postgresql's UUID type, otherwise uses - BINARY(16), to store UUID. - - """ - impl = BINARY - - def load_dialect_impl(self, dialect): - if dialect.name == 'postgresql': - return dialect.type_descriptor(psqlUUID()) - else: - return dialect.type_descriptor(BINARY(16)) - - def process_bind_param(self, value, dialect): - if value is None: - return value - else: - if not isinstance(value, uuid.UUID): - if isinstance(value, bytes): - value = uuid.UUID(bytes = value) - elif isinstance(value, int): - value = uuid.UUID(int = value) - elif isinstance(value, str): - value = uuid.UUID(value) - if dialect.name == 'postgresql': - return str(value) - else: - return value.bytes - - def process_result_value(self, value, dialect): - if value is None: - return value - if dialect.name == 'postgresql': - if isinstance(value, uuid.UUID): - return value - return uuid.UUID(value) - else: - return uuid.UUID(bytes = value) diff --git a/cognee/modules/data/models/Data.py b/cognee/modules/data/models/Data.py index 06452153..2e974560 100644 --- a/cognee/modules/data/models/Data.py +++ b/cognee/modules/data/models/Data.py @@ -2,8 +2,8 @@ from typing import List from datetime import datetime, timezone from sqlalchemy.orm import relationship, Mapped -from sqlalchemy import Column, String, DateTime -from cognee.infrastructure.databases.relational import Base, UUID +from sqlalchemy import Column, String, DateTime, UUID +from cognee.infrastructure.databases.relational import Base from .DatasetData import DatasetData class Data(Base): diff --git a/cognee/modules/data/models/Dataset.py b/cognee/modules/data/models/Dataset.py index 5cf5d235..f7078b8f 100644 --- a/cognee/modules/data/models/Dataset.py +++ b/cognee/modules/data/models/Dataset.py @@ -2,8 +2,8 @@ from typing import List from datetime import datetime, timezone from sqlalchemy.orm import relationship, Mapped -from sqlalchemy import Column, Text, DateTime -from cognee.infrastructure.databases.relational import Base, UUID +from sqlalchemy import Column, Text, DateTime, UUID +from cognee.infrastructure.databases.relational import Base from .DatasetData import DatasetData class Dataset(Base): diff --git a/cognee/modules/data/models/DatasetData.py b/cognee/modules/data/models/DatasetData.py index ed9d3c64..a35c120e 100644 --- a/cognee/modules/data/models/DatasetData.py +++ b/cognee/modules/data/models/DatasetData.py @@ -1,6 +1,6 @@ from datetime import datetime, timezone -from sqlalchemy import Column, DateTime, ForeignKey -from cognee.infrastructure.databases.relational import Base, UUID +from sqlalchemy import Column, DateTime, ForeignKey, UUID +from cognee.infrastructure.databases.relational import Base class DatasetData(Base): __tablename__ = "dataset_data" diff --git a/cognee/modules/pipelines/models/Pipeline.py b/cognee/modules/pipelines/models/Pipeline.py index e9cad594..f4d20bb0 100644 --- a/cognee/modules/pipelines/models/Pipeline.py +++ b/cognee/modules/pipelines/models/Pipeline.py @@ -1,9 +1,10 @@ from uuid import uuid4 from datetime import datetime, timezone -from sqlalchemy import Column, DateTime, String, Text +from sqlalchemy import Column, DateTime, String, Text, UUID from sqlalchemy.orm import relationship, Mapped -from cognee.infrastructure.databases.relational import Base, UUID +from cognee.infrastructure.databases.relational import Base from .PipelineTask import PipelineTask +from .Task import Task class Pipeline(Base): __tablename__ = "pipelines" diff --git a/cognee/modules/pipelines/models/PipelineRun.py b/cognee/modules/pipelines/models/PipelineRun.py index 5d5969b2..ab3498ef 100644 --- a/cognee/modules/pipelines/models/PipelineRun.py +++ b/cognee/modules/pipelines/models/PipelineRun.py @@ -1,8 +1,8 @@ import enum from uuid import uuid4 from datetime import datetime, timezone -from sqlalchemy import Column, DateTime, JSON, Enum -from cognee.infrastructure.databases.relational import Base, UUID +from sqlalchemy import Column, DateTime, JSON, Enum, UUID +from cognee.infrastructure.databases.relational import Base class PipelineRunStatus(enum.Enum): DATASET_PROCESSING_STARTED = "DATASET_PROCESSING_STARTED" diff --git a/cognee/modules/pipelines/models/PipelineTask.py b/cognee/modules/pipelines/models/PipelineTask.py index acbf44e5..c6c7eb5e 100644 --- a/cognee/modules/pipelines/models/PipelineTask.py +++ b/cognee/modules/pipelines/models/PipelineTask.py @@ -1,6 +1,6 @@ from datetime import datetime, timezone -from sqlalchemy import Column, DateTime, ForeignKey -from cognee.infrastructure.databases.relational import Base, UUID +from sqlalchemy import Column, DateTime, ForeignKey, UUID +from cognee.infrastructure.databases.relational import Base class PipelineTask(Base): __tablename__ = "pipeline_task" diff --git a/cognee/modules/search/models/Query.py b/cognee/modules/search/models/Query.py index f043dfd1..18219633 100644 --- a/cognee/modules/search/models/Query.py +++ b/cognee/modules/search/models/Query.py @@ -1,7 +1,7 @@ from uuid import uuid4 from datetime import datetime, timezone -from sqlalchemy import Column, DateTime, String -from cognee.infrastructure.databases.relational import Base, UUID +from sqlalchemy import Column, DateTime, String, UUID +from cognee.infrastructure.databases.relational import Base class Query(Base): __tablename__ = "queries" diff --git a/cognee/modules/search/models/Result.py b/cognee/modules/search/models/Result.py index 18366d24..acda59dd 100644 --- a/cognee/modules/search/models/Result.py +++ b/cognee/modules/search/models/Result.py @@ -1,7 +1,7 @@ from datetime import datetime, timezone from uuid import uuid4 -from sqlalchemy import Column, DateTime, Text -from cognee.infrastructure.databases.relational import Base, UUID +from sqlalchemy import Column, DateTime, Text, UUID +from cognee.infrastructure.databases.relational import Base class Result(Base): __tablename__ = "results" @@ -10,7 +10,7 @@ class Result(Base): value = Column(Text) query_id = Column(UUID) - user_id = Column(UUID) + user_id = Column(UUID, index = True) created_at = Column(DateTime(timezone = True), default = lambda: datetime.now(timezone.utc)) updated_at = Column(DateTime(timezone = True), onupdate = lambda: datetime.now(timezone.utc)) diff --git a/cognee/modules/users/models/ACL.py b/cognee/modules/users/models/ACL.py index b01fe601..f54d2422 100644 --- a/cognee/modules/users/models/ACL.py +++ b/cognee/modules/users/models/ACL.py @@ -1,8 +1,8 @@ from uuid import uuid4 from datetime import datetime, timezone from sqlalchemy.orm import relationship, Mapped -from sqlalchemy import Column, ForeignKey, DateTime -from cognee.infrastructure.databases.relational import Base, UUID +from sqlalchemy import Column, ForeignKey, DateTime, UUID +from cognee.infrastructure.databases.relational import Base from .ACLResources import ACLResources class ACL(Base): diff --git a/cognee/modules/users/models/ACLResources.py b/cognee/modules/users/models/ACLResources.py index 268d4a75..464fed2e 100644 --- a/cognee/modules/users/models/ACLResources.py +++ b/cognee/modules/users/models/ACLResources.py @@ -1,6 +1,6 @@ from datetime import datetime, timezone -from sqlalchemy import Column, ForeignKey, DateTime -from cognee.infrastructure.databases.relational import Base, UUID +from sqlalchemy import Column, ForeignKey, DateTime, UUID +from cognee.infrastructure.databases.relational import Base class ACLResources(Base): __tablename__ = "acl_resources" diff --git a/cognee/modules/users/models/Group.py b/cognee/modules/users/models/Group.py index d86dbee9..793decb3 100644 --- a/cognee/modules/users/models/Group.py +++ b/cognee/modules/users/models/Group.py @@ -1,6 +1,5 @@ from sqlalchemy.orm import relationship, Mapped -from sqlalchemy import Column, String, ForeignKey -from cognee.infrastructure.databases.relational import UUID +from sqlalchemy import Column, String, ForeignKey, UUID from .Principal import Principal from .UserGroup import UserGroup diff --git a/cognee/modules/users/models/Permission.py b/cognee/modules/users/models/Permission.py index 84b1a307..3b170937 100644 --- a/cognee/modules/users/models/Permission.py +++ b/cognee/modules/users/models/Permission.py @@ -1,8 +1,8 @@ from uuid import uuid4 from datetime import datetime, timezone # from sqlalchemy.orm import relationship -from sqlalchemy import Column, DateTime, String -from cognee.infrastructure.databases.relational import Base, UUID +from sqlalchemy import Column, DateTime, String, UUID +from cognee.infrastructure.databases.relational import Base class Permission(Base): __tablename__ = "permissions" diff --git a/cognee/modules/users/models/Principal.py b/cognee/modules/users/models/Principal.py index 4ef91ffa..dc6e5130 100644 --- a/cognee/modules/users/models/Principal.py +++ b/cognee/modules/users/models/Principal.py @@ -1,7 +1,7 @@ from uuid import uuid4 from datetime import datetime, timezone -from sqlalchemy import Column, String, DateTime -from cognee.infrastructure.databases.relational import Base, UUID +from sqlalchemy import Column, String, DateTime, UUID +from cognee.infrastructure.databases.relational import Base class Principal(Base): __tablename__ = "principals" diff --git a/cognee/modules/users/models/Resource.py b/cognee/modules/users/models/Resource.py index 0eca509a..563f9627 100644 --- a/cognee/modules/users/models/Resource.py +++ b/cognee/modules/users/models/Resource.py @@ -1,8 +1,8 @@ from uuid import uuid4 from datetime import datetime, timezone from sqlalchemy.orm import relationship -from sqlalchemy import Column, DateTime -from cognee.infrastructure.databases.relational import Base, UUID +from sqlalchemy import Column, DateTime, UUID +from cognee.infrastructure.databases.relational import Base from .ACLResources import ACLResources class Resource(Base): diff --git a/cognee/modules/users/models/User.py b/cognee/modules/users/models/User.py index 96f78b12..3536ac94 100644 --- a/cognee/modules/users/models/User.py +++ b/cognee/modules/users/models/User.py @@ -1,10 +1,10 @@ from uuid import UUID as uuid_UUID -from sqlalchemy import ForeignKey, Column +from sqlalchemy import ForeignKey, Column, UUID from sqlalchemy.orm import relationship, Mapped from fastapi_users.db import SQLAlchemyBaseUserTableUUID -from cognee.infrastructure.databases.relational import UUID from .Principal import Principal from .UserGroup import UserGroup +from .Group import Group class User(SQLAlchemyBaseUserTableUUID, Principal): __tablename__ = "users" @@ -25,7 +25,6 @@ class User(SQLAlchemyBaseUserTableUUID, Principal): from fastapi_users import schemas class UserRead(schemas.BaseUser[uuid_UUID]): - # groups: list[uuid_UUID] # Add groups attribute pass class UserCreate(schemas.BaseUserCreate): diff --git a/cognee/modules/users/models/UserGroup.py b/cognee/modules/users/models/UserGroup.py index a2dfa8bc..5a85c9d3 100644 --- a/cognee/modules/users/models/UserGroup.py +++ b/cognee/modules/users/models/UserGroup.py @@ -1,6 +1,6 @@ from datetime import datetime, timezone -from sqlalchemy import Column, ForeignKey, DateTime -from cognee.infrastructure.databases.relational import Base, UUID +from sqlalchemy import Column, ForeignKey, DateTime, UUID +from cognee.infrastructure.databases.relational import Base class UserGroup(Base): __tablename__ = "user_groups" From e1a32410d17f3c71641b077c83404d56f0f86853 Mon Sep 17 00:00:00 2001 From: Leon Luithlen Date: Thu, 14 Nov 2024 11:11:44 +0100 Subject: [PATCH 5/5] Add new cognee_db --- .../integration/run_toy_tasks/data/cognee_db | Bin 139264 -> 159744 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/cognee/tests/integration/run_toy_tasks/data/cognee_db b/cognee/tests/integration/run_toy_tasks/data/cognee_db index 60455ad298370fbce1f08c2ea7554de6a85116b8..912adf2955f51b156d0affe520e4b360c5554031 100644 GIT binary patch literal 159744 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 b+M6&ErShzO2FZ3R&(<^0y8r*Pw*UVRz$b%1 literal 139264 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 Z@E%BPS5!1rsV&o{V~VsCcg>+`{|}Zl+Y