diff --git a/.gitignore b/.gitignore index b36e0ad..2d15c12 100644 --- a/.gitignore +++ b/.gitignore @@ -363,7 +363,7 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ ### Python Patch ### # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration diff --git a/freetext/assignment_stores/JSONFileAssignmentStore.py b/freetext/assignment_stores/JSONFileAssignmentStore.py index 3714738..fb1aa67 100644 --- a/freetext/assignment_stores/JSONFileAssignmentStore.py +++ b/freetext/assignment_stores/JSONFileAssignmentStore.py @@ -5,6 +5,8 @@ import json import os import uuid +from typing import Union +import pathlib class JSONFileAssignmentStore(AssignmentStore): @@ -12,8 +14,8 @@ class JSONFileAssignmentStore(AssignmentStore): A AssignmentStore that stores assignments in a JSON file. """ - def __init__(self, filename: str): - self._filename = filename + def __init__(self, path: Union[str, pathlib.Path]): + self._filename = path def get_assignment(self, key: AssignmentID) -> Assignment: """ diff --git a/freetext/assignment_stores/__init__.py b/freetext/assignment_stores/__init__.py index 09aa179..52ba03c 100644 --- a/freetext/assignment_stores/__init__.py +++ b/freetext/assignment_stores/__init__.py @@ -1,4 +1,56 @@ from .AssignmentStore import AssignmentStore, InMemoryAssignmentStore from .JSONFileAssignmentStore import JSONFileAssignmentStore +from .DynamoAssignmentStore import DynamoAssignmentStore +from pydantic import BaseModel +from typing import Literal, Union -__all__ = ["AssignmentStore", "InMemoryAssignmentStore", "JSONFileAssignmentStore"] + +class InMemoryAssignmentStoreConfig(BaseModel): + type: str = Literal["in_memory"] + + +class JSONAssignmentStoreConfig(BaseModel): + type: str = Literal["json"] + path: str = "assignments.json" + + +class DynamoAssignmentStoreConfig(BaseModel): + type: str = Literal["dynamo"] + aws_access_key_id: str + aws_secret_access_key: str + aws_region: str + table_name: str + + +AssignmentStoreConfig = Union[ + InMemoryAssignmentStoreConfig, + JSONAssignmentStoreConfig, + DynamoAssignmentStoreConfig, +] + + +def create_assignment_store(config: AssignmentStoreConfig) -> AssignmentStore: + """Factory function for creating a assignment store from a config object.""" + if config.type == Literal["in_memory"]: + return InMemoryAssignmentStore() + elif config.type == Literal["json"]: + return JSONFileAssignmentStore(config.path) + elif config.type == Literal["dynamo"]: + return DynamoAssignmentStore( + config.aws_access_key_id, + config.aws_secret_access_key, + config.aws_region, + config.table_name, + ) + else: + raise ValueError(f"Unknown assignment store type: {config.type}") + + +__all__ = [ + "AssignmentStore", + "InMemoryAssignmentStore", + "JSONFileAssignmentStore", + "DynamoAssignmentStore", + "AssignmentStoreConfig", + "create_assignment_store", +] diff --git a/freetext/config.example.py b/freetext/config.example.py index 0f983c0..2264256 100644 --- a/freetext/config.example.py +++ b/freetext/config.example.py @@ -1,4 +1,6 @@ -from pydantic import BaseSettings +from pydantic import BaseSettings, Field +from freetext.assignment_stores import AssignmentStoreConfig +from freetext.response_stores import ResponseStoreConfig class OpenAIConfig(BaseSettings): @@ -13,11 +15,9 @@ class ApplicationSettings(BaseSettings): # your OpenAI API key) assignment_creation_secret: str = "I'm totally allowed to make a project" - # AWS credentials and table names for storing assignments and responses. - # If you're using local (e.g., JSON-based) stores, you can set these all to - # empty strings or ignore them entirely. - aws_access_key_id: str = "AKIA###" - aws_secret_access_key: str = "###" - aws_region: str = "us-east-1" - assignments_table: str = "llm4_freetext_assignments" - responses_table: str = "llm4_freetext_responses" + # To override the config for stores, replace Field(..., discriminator="type") with the config you want, e.g.: + # assignment_store: AssignmentStoreConfig = JSONAssignmentStoreConfig(path="assignments.json") + # or + # assignment_store: AssignmentStoreConfig = InMemoryAssignmentStoreConfig() + assignment_store: AssignmentStoreConfig = Field(..., discriminator="type") + response_store: ResponseStoreConfig = Field(..., discriminator="type") diff --git a/freetext/response_stores/__init__.py b/freetext/response_stores/__init__.py index 7304394..1b16da4 100644 --- a/freetext/response_stores/__init__.py +++ b/freetext/response_stores/__init__.py @@ -1,2 +1,54 @@ from .ResponseStore import ResponseStore, InMemoryResponseStore from .JSONFileResponseStore import JSONFileResponseStore +from .DynamoResponseStore import DynamoResponseStore +from pydantic import BaseModel +from typing import Literal, Union + + +class InMemoryResponseStoreConfig(BaseModel): + type: str = Literal["in_memory"] + + +class JSONResponseStoreConfig(BaseModel): + type: str = Literal["json"] + path: str = "responses.json" + + +class DynamoResponseStoreConfig(BaseModel): + type: str = Literal["dynamo"] + aws_access_key_id: str + aws_secret_access_key: str + aws_region: str + table_name: str + + +ResponseStoreConfig = Union[ + InMemoryResponseStoreConfig, JSONResponseStoreConfig, DynamoResponseStoreConfig +] + + +def create_response_store(config: ResponseStoreConfig) -> ResponseStore: + """Factory function for creating a response store from a config object.""" + if config.type == Literal["in_memory"]: + return InMemoryResponseStore() + elif config.type == Literal["json"]: + return JSONFileResponseStore(config.path) + elif config.type == Literal["dynamo"]: + return DynamoResponseStore( + config.aws_access_key_id, + config.aws_secret_access_key, + config.aws_region, + config.table_name, + ) + else: + raise ValueError(f"Unknown response store type: {config.type}") + + +__all__ = [ + "ResponseStore", + "InMemoryResponseStore", + "JSONFileResponseStore", + "DynamoResponseStore", + "ResponseStoreConfig", + "create_response_store", +] diff --git a/freetext/server.py b/freetext/server.py index d0b39bd..0c783cb 100644 --- a/freetext/server.py +++ b/freetext/server.py @@ -6,19 +6,9 @@ from fastapi.responses import HTMLResponse, PlainTextResponse from mangum import Mangum -from .assignment_stores import ( - AssignmentStore, - InMemoryAssignmentStore, - JSONFileAssignmentStore, -) -from .assignment_stores.DynamoAssignmentStore import DynamoAssignmentStore +from freetext.assignment_stores import AssignmentStore, create_assignment_store +from freetext.response_stores import ResponseStore, create_response_store from .config import ApplicationSettings -from .response_stores.ResponseStore import ( - ResponseStore, - InMemoryResponseStore, -) -from .response_stores.DynamoResponseStore import DynamoResponseStore - from .feedback_providers.FeedbackProvider import FeedbackProvider from .feedback_providers.OpenAIFeedbackProvider import OpenAIChatBasedFeedbackProvider from .llm4text_types import ( @@ -39,9 +29,9 @@ class FeedbackRouter: def __init__( self, + assignment_store: AssignmentStore, + response_store: ResponseStore, feedback_providers: Optional[list[FeedbackProvider]] = None, - assignment_store: Optional[AssignmentStore] = None, - response_store: Optional[ResponseStore] = None, fallback_feedback_provider: Optional[FeedbackProvider] = None, ): """ @@ -53,15 +43,9 @@ def __init__( assignment_store: An assignment store to use. """ + self._assignment_store = assignment_store + self._response_store = response_store self._feedback_providers = feedback_providers or [] - self._assignment_store = ( - assignment_store - if (assignment_store is not None) - else InMemoryAssignmentStore() - ) - self._response_store = ( - response_store if (response_store is not None) else InMemoryResponseStore() - ) self._fallback_feedback_provider = fallback_feedback_provider def add_feedback_provider(self, feedback_provider: FeedbackProvider) -> None: @@ -120,20 +104,9 @@ def get_commons(): config = ApplicationSettings() return Commons( feedback_router=FeedbackRouter( - [OpenAIChatBasedFeedbackProvider()], - assignment_store=DynamoAssignmentStore( - aws_access_key_id=config.aws_access_key_id, - aws_secret_access_key=config.aws_secret_access_key, - aws_region=config.aws_region, - table_name=config.assignments_table, - ), - response_store=DynamoResponseStore( - aws_access_key_id=config.aws_access_key_id, - aws_secret_access_key=config.aws_secret_access_key, - aws_region=config.aws_region, - table_name=config.responses_table, - ), - # JSONFileAssignmentStore("assignments.json"), + assignment_store=create_assignment_store(config.assignment_store), + response_store=create_response_store(config.response_store), + feedback_providers=[OpenAIChatBasedFeedbackProvider()], ) )