great-ai/great_ai/persistence/mongodb_driver.py
Andras Schmelczer 8faee98ec6
Some checks failed
Publish documentation / publish (push) Successful in 58s
Check / Lint, format & type checks (push) Failing after 1m2s
Check / Test on Python 3.10 (push) Successful in 1m9s
Check / Test on Python 3.9 (push) Successful in 50s
Modernise
2026-06-06 21:39:06 +01:00

173 lines
5.7 KiB
Python

from datetime import datetime
from typing import Any, Dict, List, Optional, Sequence, Tuple
from pymongo import ASCENDING, DESCENDING, MongoClient
from ..utilities import chunk
from ..views import Filter, SortBy, Trace
from .tracing_database_driver import TracingDatabaseDriver
operator_mapping = {
"=": "$eq",
"!=": "$ne",
"<": "$lt",
"<=": "$lte",
">": "$gt",
">=": "$gte",
"contains": "$regex",
}
class MongoDbDriver(TracingDatabaseDriver):
"""TracingDatabaseDriver implementation using MongoDB as a backend.
A production-ready database driver suitable for efficiently handling semi-structured
data.
Checkout [MongoDB Atlas](https://www.mongodb.com/cloud/atlas/register) for a hosted
MongoDB solution.
"""
is_production_ready = True
mongo_connection_string: str
mongo_database: str
def __init__(self) -> None:
super().__init__()
if self.mongo_connection_string is None or self.mongo_database is None:
raise ValueError(
"Please configure the MongoDB access options by calling "
"MongoDbDriver.configure_credentials"
)
with MongoClient[Any](self.mongo_connection_string) as client:
client[self.mongo_database].traces.create_index(
[("tags", ASCENDING), ("created", DESCENDING)], background=True
)
@classmethod
def configure_credentials( # type: ignore
cls,
*,
mongo_connection_string: str,
mongo_database: str,
**_: Any,
) -> None:
"""Configure the connection details for MongoDB.
Args:
mongo_connection_string: For example:
'mongodb://my_user:my_pass@localhost:27017'
mongo_database: Name of the database to use. If doesn't exist, it is
created and initialised.
"""
cls.mongo_connection_string = mongo_connection_string
cls.mongo_database = mongo_database
super().configure_credentials()
def save(self, trace: Trace) -> str:
serialized = trace.to_flat_dict()
serialized["_id"] = trace.trace_id
with MongoClient[Any](self.mongo_connection_string) as client:
return client[self.mongo_database].traces.insert_one(serialized).inserted_id
def save_batch(self, documents: List[Trace]) -> List[str]:
serialized = [d.to_flat_dict() for d in documents]
for s in serialized:
s["_id"] = s["trace_id"]
with MongoClient[Any](self.mongo_connection_string) as client:
return (
client[self.mongo_database]
.traces.insert_many(serialized, ordered=False)
.inserted_ids
)
def get(self, id: str) -> Optional[Trace]:
with MongoClient[Any](self.mongo_connection_string) as client:
value = client[self.mongo_database].traces.find_one(id)
if value:
value = Trace.model_validate(value)
return value
def _get_operator(self, filter: Filter) -> str:
if filter.operator == "contains" and not isinstance(filter.value, str):
return operator_mapping["="]
return operator_mapping[filter.operator]
def query(
self,
*,
skip: int = 0,
take: Optional[int] = None,
conjunctive_filters: Sequence[Filter] = [],
conjunctive_tags: Sequence[str] = [],
since: Optional[datetime] = None,
until: Optional[datetime] = None,
has_feedback: Optional[bool] = None,
sort_by: Sequence[SortBy] = [],
) -> Tuple[List[Trace], int]:
query: Dict[str, Any] = {
"filter": {},
}
and_query: List[Dict[str, Any]] = []
and_query.extend({"tags": tag} for tag in conjunctive_tags)
and_query.extend(
{f.property: {self._get_operator(f): f.value}} for f in conjunctive_filters
)
if not and_query:
and_query.append({})
if since:
and_query.append({"created": {"$gte": since}})
if until:
and_query.append({"created": {"$lte": until}})
if has_feedback is not None:
and_query.append(
{"feedback": {"$ne": None}} if has_feedback else {"feedback": None}
)
query["filter"]["$and"] = and_query
with MongoClient[Any](self.mongo_connection_string) as client:
count = client[self.mongo_database].traces.count_documents(**query)
if skip:
query["skip"] = skip
if take:
query["limit"] = take
query["sort"] = [
(col.column_id, 1 if col.direction == "asc" else -1) for col in sort_by
]
with client[self.mongo_database].traces.find(**query) as cursor:
documents = [Trace[Any].model_validate(t) for t in cursor]
return documents, count
def update(self, id: str, new_version: Trace) -> None:
serialized = new_version.to_flat_dict()
serialized["_id"] = new_version.trace_id
with MongoClient[Any](self.mongo_connection_string) as client:
client[self.mongo_database].traces.update_one({"_id": id}, serialized)
def delete(self, id: str) -> None:
with MongoClient[Any](self.mongo_connection_string) as client:
client[self.mongo_database].traces.delete_one({"_id": id})
def delete_batch(self, ids: List[str]) -> None:
with MongoClient[Any](self.mongo_connection_string) as client:
for c in chunk(
ids, chunk_size=10000
): # avoid: 'delete' command document too large
delete_filter = {"_id": {"$in": c}}
client[self.mongo_database].traces.delete_many(delete_filter)