152 lines
4.6 KiB
Python
152 lines
4.6 KiB
Python
from abc import abstractmethod
|
|||
|
|
from dataclasses import dataclass, field
|
||
|
|
from datetime import datetime
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
from gfa_core.mas.memory.base import BaseMemory, MemoryEntry, SearchResult
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class KnowledgeEntry:
|
||
|
|
entry_id: str
|
||
|
|
content: str
|
||
|
|
embedding: list[float] | None = None
|
||
|
|
source: str | None = None
|
||
|
|
category: str | None = None
|
||
|
|
tags: list[str] = field(default_factory=list)
|
||
|
|
created_at: datetime = field(default_factory=datetime.now)
|
||
|
|
updated_at: datetime = field(default_factory=datetime.now)
|
||
|
|
metadata: dict[str, Any] = field(default_factory=dict)
|
||
|
|
|
||
|
|
|
||
|
|
class KnowledgeMemory(BaseMemory):
|
||
|
|
def __init__(self, embedding_dim: int = 1536):
|
||
|
|
self._embedding_dim = embedding_dim
|
||
|
|
self._entries: dict[str, MemoryEntry[KnowledgeEntry]] = {}
|
||
|
|
self._embeddings: dict[str, list[float]] = {}
|
||
|
|
|
||
|
|
async def store(self, key: str, value: Any, ttl: int | None = None) -> None:
|
||
|
|
if isinstance(value, KnowledgeEntry):
|
||
|
|
entry = value
|
||
|
|
else:
|
||
|
|
entry = KnowledgeEntry(entry_id=key, **value)
|
||
|
|
|
||
|
|
memory_entry = MemoryEntry(key=key, value=entry)
|
||
|
|
self._entries[key] = memory_entry
|
||
|
|
|
||
|
|
if entry.embedding:
|
||
|
|
self._embeddings[key] = entry.embedding
|
||
|
|
|
||
|
|
async def retrieve(self, key: str) -> Any | None:
|
||
|
|
entry = self._entries.get(key)
|
||
|
|
return entry.value if entry else None
|
||
|
|
|
||
|
|
async def delete(self, key: str) -> bool:
|
||
|
|
if key in self._entries:
|
||
|
|
del self._entries[key]
|
||
|
|
self._embeddings.pop(key, None)
|
||
|
|
return True
|
||
|
|
return False
|
||
|
|
|
||
|
|
async def exists(self, key: str) -> bool:
|
||
|
|
return key in self._entries
|
||
|
|
|
||
|
|
async def clear(self) -> None:
|
||
|
|
self._entries.clear()
|
||
|
|
self._embeddings.clear()
|
||
|
|
|
||
|
|
async def search(
|
||
|
|
self,
|
||
|
|
query_embedding: list[float],
|
||
|
|
top_k: int = 10,
|
||
|
|
threshold: float = 0.0,
|
||
|
|
) -> list[SearchResult]:
|
||
|
|
if not self._embeddings:
|
||
|
|
return []
|
||
|
|
|
||
|
|
scores = []
|
||
|
|
for key, embedding in self._embeddings.items():
|
||
|
|
score = self._cosine_similarity(query_embedding, embedding)
|
||
|
|
if score >= threshold:
|
||
|
|
scores.append((key, score))
|
||
|
|
|
||
|
|
scores.sort(key=lambda x: x[1], reverse=True)
|
||
|
|
top_results = scores[:top_k]
|
||
|
|
|
||
|
|
results = []
|
||
|
|
for key, score in top_results:
|
||
|
|
entry = self._entries.get(key)
|
||
|
|
if entry:
|
||
|
|
results.append(SearchResult(
|
||
|
|
key=key,
|
||
|
|
value=entry.value,
|
||
|
|
score=score,
|
||
|
|
metadata=entry.metadata,
|
||
|
|
))
|
||
|
|
|
||
|
|
return results
|
||
|
|
|
||
|
|
def _cosine_similarity(self, a: list[float], b: list[float]) -> float:
|
||
|
|
if len(a) != len(b):
|
||
|
|
return 0.0
|
||
|
|
|
||
|
|
dot_product = sum(x * y for x, y in zip(a, b))
|
||
|
|
norm_a = sum(x * x for x in a) ** 0.5
|
||
|
|
norm_b = sum(x * x for x in b) ** 0.5
|
||
|
|
|
||
|
|
if norm_a == 0 or norm_b == 0:
|
||
|
|
return 0.0
|
||
|
|
|
||
|
|
return dot_product / (norm_a * norm_b)
|
||
|
|
|
||
|
|
async def search_by_tags(self, tags: list[str], top_k: int = 10) -> list[SearchResult]:
|
||
|
|
results = []
|
||
|
|
for key, entry in self._entries.items():
|
||
|
|
if entry.value.tags and any(tag in entry.value.tags for tag in tags):
|
||
|
|
results.append(SearchResult(
|
||
|
|
key=key,
|
||
|
|
value=entry.value,
|
||
|
|
score=1.0,
|
||
|
|
metadata=entry.metadata,
|
||
|
|
))
|
||
|
|
if len(results) >= top_k:
|
||
|
|
break
|
||
|
|
|
||
|
|
return results
|
||
|
|
|
||
|
|
async def search_by_category(self, category: str, top_k: int = 10) -> list[SearchResult]:
|
||
|
|
results = []
|
||
|
|
for key, entry in self._entries.items():
|
||
|
|
if entry.value.category == category:
|
||
|
|
results.append(SearchResult(
|
||
|
|
key=key,
|
||
|
|
value=entry.value,
|
||
|
|
score=1.0,
|
||
|
|
metadata=entry.metadata,
|
||
|
|
))
|
||
|
|
if len(results) >= top_k:
|
||
|
|
break
|
||
|
|
|
||
|
|
return results
|
||
|
|
|
||
|
|
async def add_entry(
|
||
|
|
self,
|
||
|
|
entry_id: str,
|
||
|
|
content: str,
|
||
|
|
embedding: list[float] | None = None,
|
||
|
|
source: str | None = None,
|
||
|
|
category: str | None = None,
|
||
|
|
tags: list[str] | None = None,
|
||
|
|
metadata: dict[str, Any] | None = None,
|
||
|
|
) -> None:
|
||
|
|
entry = KnowledgeEntry(
|
||
|
|
entry_id=entry_id,
|
||
|
|
content=content,
|
||
|
|
embedding=embedding,
|
||
|
|
source=source,
|
||
|
|
category=category,
|
||
|
|
tags=tags or [],
|
||
|
|
metadata=metadata or {},
|
||
|
|
)
|
||
|
|
await self.store(entry_id, entry)
|