Skip to content
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

Deletes unused FileStorage and ModelStorage apis #106

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"folders": [
{
"path": ".",
"name": "assistants/prospector-assistant"
"name": "assistants/guided-conversation-assistant"
},
{
"path": "../.."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
AssistantConversationInspectorStateDataModel,
BaseModelAssistantConfig,
ConversationContext,
FileStorageContext,
storage_directory_for_context,
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -143,7 +143,7 @@ def _get_guided_conversation_storage_path(context: ConversationContext, filename
"""
Get the path to the directory for storing guided conversation files.
"""
path = FileStorageContext.get(context).directory / "guided-conversation"
path = storage_directory_for_context(context) / "guided-conversation"
if filename:
path /= filename
return path
Expand Down
2 changes: 1 addition & 1 deletion assistants/guided-conversation-assistant/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "assistant"
version = "0.1.0"
description = "Exploration of a python Semantic Workbench OpenAI assistant to help mine artifacts for ideas."
description = "Demonstrates how to guide a user towards a goal using the guided-conversation library."
authors = [{ name = "Semantic Workbench Team" }]
readme = "README.md"
requires-python = ">=3.11"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
AssistantConversationInspectorStateDataModel,
BaseModelAssistantConfig,
ConversationContext,
FileStorageContext,
storage_directory_for_context,
)
from semantic_workbench_assistant.config import UISchema
from semantic_workbench_assistant.storage import read_model, write_model
Expand Down Expand Up @@ -294,10 +294,12 @@ def _get_artifact_storage_path(context: ConversationContext, filename: str | Non
"""
Get the path to the directory for storing artifacts.
"""
path = FileStorageContext.get(context).directory / "artifacts"
if filename:
path /= filename
return path
path = storage_directory_for_context(context) / "artifacts"

if not filename:
return path

return path / filename


# endregion
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from semantic_workbench_api_model.workbench_model import File
from semantic_workbench_assistant.assistant_app import (
ConversationContext,
FileStorageContext,
storage_directory_for_context,
)
from semantic_workbench_assistant.config import UISchema

Expand Down Expand Up @@ -235,7 +235,7 @@ def _get_attachment_drive(context: ConversationContext) -> Drive:
Get the Drive instance for the attachments.
"""
drive_context = Context(session_id=context.id)
drive_root = str(FileStorageContext.get(context).directory / "attachments")
drive_root = str(storage_directory_for_context(context) / "attachments")
return Drive(DriveConfig(context=drive_context, root=drive_root))


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
AssistantConversationInspectorStateDataModel,
BaseModelAssistantConfig,
ConversationContext,
FileStorageContext,
storage_directory_for_context,
)

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -251,7 +251,7 @@ def _get_guided_conversation_storage_path(context: ConversationContext, filename
"""
Get the path to the directory for storing guided conversation files.
"""
path = FileStorageContext.get(context).directory / "guided-conversation"
path = storage_directory_for_context(context) / "guided-conversation"
if filename:
path /= filename
return path
Expand Down
4 changes: 2 additions & 2 deletions assistants/skill-assistant/assistant/skill_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
)
from semantic_workbench_assistant.assistant_app import (
ConversationContext,
FileStorageContext,
storage_directory_for_context,
)
from skill_library import Assistant

Expand Down Expand Up @@ -134,7 +134,7 @@ async def create_assistant(
)
return

data_dir = FileStorageContext.get(conversation_context).directory
data_dir = storage_directory_for_context(conversation_context)

# Create the assistant.
assistant = Assistant(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +0,0 @@
import semantic_workbench_assistant
import semantic_workbench_assistant.storage

file_storage = semantic_workbench_assistant.storage.FileStorage(settings=semantic_workbench_assistant.settings.storage)
Original file line number Diff line number Diff line change
@@ -1,4 +0,0 @@
import semantic_workbench_assistant
import semantic_workbench_assistant.storage

file_storage = semantic_workbench_assistant.storage.FileStorage(settings=semantic_workbench_assistant.settings.storage)
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from . import settings, storage
from . import settings

settings = settings.Settings()
file_storage = storage.FileStorage(settings=settings.storage)
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
ContentSafetyEvaluationResult,
ContentSafetyEvaluator,
)
from .context import AssistantContext, ConversationContext, FileStorageContext
from .context import AssistantContext, ConversationContext, storage_directory_for_context
from .error import BadRequestError, ConflictError, NotFoundError
from .export_import import FileStorageAssistantDataExporter, FileStorageConversationDataExporter
from .protocol import (
Expand Down Expand Up @@ -36,5 +36,5 @@
"BadRequestError",
"NotFoundError",
"ConflictError",
"FileStorageContext",
"storage_directory_for_context",
]
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
replace_config_secret_str_masked_values,
)
from ..storage import read_model, write_model
from .context import AssistantContext, FileStorageContext
from .context import AssistantContext, storage_directory_for_context
from .error import BadRequestError
from .protocol import (
AssistantConfigDataModel,
Expand All @@ -37,11 +37,11 @@ def __init__(self, cls: type[ConfigModelT]) -> None:

def _private_path_for(self, assistant_context: AssistantContext) -> pathlib.Path:
# store assistant config, including secrets, in a separate partition that is never exported
return FileStorageContext.get(assistant_context, partition="private").directory / "config.json"
return storage_directory_for_context(assistant_context, partition="private") / "config.json"

def _export_import_path_for(self, assistant_context: AssistantContext) -> pathlib.Path:
# store a copy of the config for export in the standard partition
return FileStorageContext.get(assistant_context).directory / "config.json"
return storage_directory_for_context(assistant_context) / "config.json"

async def get(self, assistant_context: AssistantContext) -> ConfigModelT:
path = self._private_path_for(assistant_context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,20 +95,15 @@ async def delete_file(self, filename: str) -> None:
return await self._workbench_client.delete_file(filename)


@dataclass
class FileStorageContext:
directory: pathlib.Path

@staticmethod
def get(context: AssistantContext | ConversationContext, partition: str = "") -> "FileStorageContext":
match context:
case AssistantContext():
directory = context.id
def storage_directory_for_context(context: AssistantContext | ConversationContext, partition: str = "") -> pathlib.Path:
match context:
case AssistantContext():
directory = context.id

case ConversationContext():
directory = f"{context.assistant.id}-{context.id}"
case ConversationContext():
directory = f"{context.assistant.id}-{context.id}"

if partition:
directory = f"{directory}_{partition}"
if partition:
directory = f"{directory}_{partition}"

return FileStorageContext(directory=pathlib.Path(settings.storage.root) / directory)
return pathlib.Path(settings.storage.root) / directory
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
AsyncIterator,
)

from .context import AssistantContext, ConversationContext, FileStorageContext
from .context import AssistantContext, ConversationContext, storage_directory_for_context
from .error import BadRequestError

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -70,11 +70,11 @@ class FileStorageAssistantDataExporter:

@asynccontextmanager
async def export(self, context: AssistantContext) -> AsyncIterator[IO[bytes]]:
async with zip_directory(FileStorageContext.get(context).directory) as stream:
async with zip_directory(storage_directory_for_context(context)) as stream:
yield stream

async def import_(self, context: AssistantContext, stream: IO[bytes]) -> None:
await unzip_to_directory(stream, FileStorageContext.get(context).directory)
await unzip_to_directory(stream, storage_directory_for_context(context))


class FileStorageConversationDataExporter:
Expand All @@ -85,8 +85,8 @@ class FileStorageConversationDataExporter:

@asynccontextmanager
async def export(self, context: ConversationContext) -> AsyncIterator[IO[bytes]]:
async with zip_directory(FileStorageContext.get(context).directory) as stream:
async with zip_directory(storage_directory_for_context(context)) as stream:
yield stream

async def import_(self, context: ConversationContext, stream: IO[bytes]) -> None:
await unzip_to_directory(stream, FileStorageContext.get(context).directory)
await unzip_to_directory(stream, storage_directory_for_context(context))
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import hashlib
import io
import logging
import os
import pathlib
from contextlib import contextmanager
from typing import Any, BinaryIO, Generic, Iterator, TypeVar
from typing import Any, Iterator, TypeVar

from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict
Expand All @@ -16,63 +13,6 @@ class FileStorageSettings(BaseSettings):
model_config = SettingsConfigDict(extra="allow")

root: str = ".data/files"
ensure_safe_filenames: bool = True


class FileStorage:
def __init__(self, settings: FileStorageSettings):
self.root = pathlib.Path(settings.root)
self._ensure_safe_filenames = settings.ensure_safe_filenames

def _path_for(self, dir: str, filename: str, mkdir=False) -> pathlib.Path:
namespace_path = self.root / dir
if mkdir:
namespace_path.mkdir(parents=True, exist_ok=True)

if not filename:
return namespace_path

if not self._ensure_safe_filenames:
return namespace_path / filename

filename_hash = hashlib.sha256(filename.encode("utf-8")).hexdigest()
return namespace_path / filename_hash

def write_file(self, dir: str, filename: str, content: BinaryIO) -> None:
file_path = self._path_for(dir, filename, mkdir=True)
with open(file_path, "wb") as f:
f.write(content.read())

def delete_file(self, dir: str, filename: str) -> None:
file_path = self._path_for(dir, filename)
file_path.unlink(missing_ok=True)

def read_all_files(self, dir: str) -> Iterator[BinaryIO]:
dir_path = self._path_for(dir, "")
if not dir_path.is_dir():
return

for file_path in dir_path.iterdir():
with open(file_path, "rb") as f:
yield f

def list_files(self, dir: str) -> Iterator[str]:
dir_path = self._path_for(dir, "")
if not dir_path.is_dir():
return

for file_path in dir_path.iterdir():
yield file_path.name

@contextmanager
def read_file(self, dir: str, filename: str) -> Iterator[BinaryIO]:
file_path = self._path_for(dir, filename)
with open(file_path, "rb") as f:
yield f

def file_exists(self, dir: str, filename: str) -> bool:
file_path = self._path_for(dir, filename)
return file_path.exists()


def write_model(file_path: os.PathLike, value: BaseModel, serialization_context: dict[str, Any] | None = None) -> None:
Expand Down Expand Up @@ -110,62 +50,3 @@ def read_models_in_dir(dir_path: os.PathLike, cls: type[ModelT]) -> Iterator[Mod
value = read_model(file_path, cls)
if value is not None:
yield value


class ModelStorage(Generic[ModelT]):
"""
Provides file-system storage for pydantic models as a utility for assistant service
implementations.
"""

def __init__(self, cls: type[ModelT], file_storage: FileStorage, namespace: str) -> None:
self._cls = cls
self._file_storage = file_storage
self._namespace = namespace

def get_all(self) -> list[ModelT]:
"""
Gets all the model values in the storage.
"""
values = []
try:
for file in self._file_storage.read_all_files(dir=self._namespace):
contents = file.read().decode("utf-8")
value = self._cls.model_validate_json(contents)
values.append(value)
except FileNotFoundError:
pass

return values

def get(self, key: str, strict: bool | None = None) -> ModelT | None:
"""
Gets the model value for the given key, or None if the key does not exist. If the
model has changed since the value was written, and is no longer valid, this will
raise pydantic.ValidationError.
"""
try:
with self._file_storage.read_file(dir=self._namespace, filename=key) as file:
contents = file.read().decode("utf-8")

except FileNotFoundError:
return None

value = self._cls.model_validate_json(contents, strict=strict)
return value

def __getitem__(self, key: str) -> ModelT:
value = self.get(key)
if value is None:
raise KeyError(key)
return value

def set(self, key: str, value: ModelT) -> None:
data_json = value.model_dump_json()
self._file_storage.write_file(dir=self._namespace, filename=key, content=io.BytesIO(data_json.encode("utf-8")))

def __setitem__(self, key: str, value: ModelT) -> None:
self.set(key=key, value=value)

def delete(self, key: str) -> None:
self._file_storage.delete_file(dir=self._namespace, filename=key)
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,3 @@ def storage_settings(request: pytest.FixtureRequest) -> Iterator[storage.FileSto
with tempfile.TemporaryDirectory() as temp_dir:
storage_settings.root = temp_dir
yield storage_settings


@pytest.fixture
def file_storage(monkeypatch: pytest.MonkeyPatch, storage_settings: storage.FileStorageSettings) -> storage.FileStorage:
monkeypatch.setattr(settings, "storage", storage_settings)

return storage.FileStorage(settings.storage)
Loading