packages feed

ollama-haskell-0.2.0.0: src/Data/Ollama/Conversation.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}

{- |
Module      : Data.Ollama.Conversation
Copyright   : (c) 2025 Tushar Adhatrao
License     : MIT
Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
Stability   : experimental
Description : Conversation management for the Ollama client, including storage and retrieval of chat sessions.

This module provides types and functions for managing conversations in the Ollama client. It defines
a 'Conversation' type to represent a chat session, a 'ConversationStore' typeclass for storage operations,
and an in-memory implementation using 'InMemoryStore' and 'ConvoM'. The module supports saving, loading,
listing, and deleting conversations, with thread-safe operations using STM (Software Transactional Memory).

The 'Conversation' type includes metadata such as a unique ID, messages, model name, and timestamps.
The 'ConversationStore' typeclass defines a generic interface for conversation storage, while 'InMemoryStore'
provides a concrete in-memory implementation. The 'ConvoM' monad integrates with 'InMemoryStore' for
monadic operations.

Example:

>>> store <- initInMemoryStore
>>> let conv = Conversation "conv1" [userMessage "Hello!"] "gemma3" <$> getCurrentTime <*> getCurrentTime
>>> runInMemoryConvo store $ saveConversation conv
>>> runInMemoryConvo store $ loadConversation "conv1"
Just (Conversation ...)
-}
module Data.Ollama.Conversation
  ( -- * Conversation Types
    Conversation (..)
  , ConversationStore (..)

    -- * In-Memory Store
  , InMemoryStore (..)
  , ConvoM (..)
  , initInMemoryStore
  , runInMemoryConvo

    -- * Validation
  , validateConversation
  ) where

import Control.Concurrent.STM
import Control.Monad.Reader
import Data.Aeson (FromJSON, ToJSON)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Ollama.Common.Types
import Data.Text (Text)
import Data.Text qualified as T
import Data.Time (UTCTime, getCurrentTime)
import GHC.Generics (Generic)

{- | Represents a chat session with metadata and messages.

Stores a conversation's unique identifier, list of messages, model name, creation time, and last updated time.
-}
data Conversation = Conversation
  { conversationId :: !Text
  -- ^ Unique identifier for the conversation.
  , messages :: ![Message]
  -- ^ List of messages in the conversation.
  , model :: !Text
  -- ^ Name of the model used in the conversation (e.g., "gemma3").
  , createdAt :: !UTCTime
  -- ^ Timestamp when the conversation was created.
  , lastUpdated :: !UTCTime
  -- ^ Timestamp when the conversation was last updated.
  }
  deriving (Show, Eq, Generic)

instance ToJSON Conversation
instance FromJSON Conversation

{- | Typeclass defining operations for storing and managing conversations.

Provides methods for saving, loading, listing, and deleting conversations in a monadic context.
--
-- @since 0.2.0.0
-}
class Monad m => ConversationStore m where
  -- | Saves a conversation to the store.
  --
  -- Validates the conversation and updates its 'lastUpdated' timestamp before saving.
  saveConversation :: Conversation -> m ()

  -- | Loads a conversation by its ID.
  --
  -- Returns 'Just' the conversation if found, or 'Nothing' if not.
  loadConversation :: Text -> m (Maybe Conversation)

  -- | Lists all conversations in the store.
  listConversations :: m [Conversation]

  -- | Deletes a conversation by its ID.
  --
  -- Returns 'True' if the conversation was found and deleted, 'False' otherwise.
  deleteConversation :: Text -> m Bool

{- | In-memory conversation store using a 'TVar' for thread-safe operations.

Stores conversations in a 'Map' keyed by conversation IDs, wrapped in a 'TVar' for concurrent access.
-}
newtype InMemoryStore = InMemoryStore (TVar (Map Text Conversation))

{- | Monad for operations with 'InMemoryStore'.

A wrapper around 'ReaderT' that provides access to an 'InMemoryStore' in a monadic context.
-}
newtype ConvoM a = ConvoM {runConvoM :: ReaderT InMemoryStore IO a}
  deriving (Functor, Applicative, Monad, MonadIO, MonadReader InMemoryStore)

{- | Runs a 'ConvoM' action with the given 'InMemoryStore'.

Executes a monadic computation in the context of an in-memory store.

Example:

>>> store <- initInMemoryStore
>>> runInMemoryConvo store $ saveConversation conv
-}
runInMemoryConvo :: InMemoryStore -> ConvoM a -> IO a
runInMemoryConvo store = flip runReaderT store . runConvoM

instance ConversationStore ConvoM where
  saveConversation conv = do
    case validateConversation conv of
      Left err -> liftIO $ putStrLn ("Validation error: " <> T.unpack err)
      Right validConv -> do
        now <- liftIO getCurrentTime
        let updatedConv = validConv {lastUpdated = now}
        InMemoryStore ref <- ask
        liftIO . atomically $ modifyTVar' ref (Map.insert (conversationId updatedConv) updatedConv)

  loadConversation cid = do
    InMemoryStore ref <- ask
    convs <- liftIO $ readTVarIO ref
    return $ Map.lookup cid convs

  listConversations = do
    InMemoryStore ref <- ask
    convs <- liftIO $ readTVarIO ref
    return $ Map.elems convs

  deleteConversation cid = do
    InMemoryStore ref <- ask
    liftIO . atomically $ do
      convs <- readTVar ref
      if Map.member cid convs
        then do
          writeTVar ref (Map.delete cid convs)
          return True
        else return False

{- | Validates a 'Conversation' to ensure required fields are non-empty.

Checks that the 'conversationId' is not empty and that the 'messages' list contains at least one message.
Returns 'Right' with the validated conversation or 'Left' with an error message.

Example:

>>> let conv = Conversation "" [] "gemma3" time time
>>> validateConversation conv
Left "Conversation ID cannot be empty"
-}
validateConversation :: Conversation -> Either Text Conversation
validateConversation conv
  | T.null (conversationId conv) = Left "Conversation ID cannot be empty"
  | null (messages conv) = Left "Conversation must have at least one message"
  | otherwise = Right conv

{- | Creates a new empty in-memory conversation store.

Initializes an 'InMemoryStore' with an empty 'Map' wrapped in a 'TVar' for thread-safe operations.

Example:

>>> store <- initInMemoryStore
>>> runInMemoryConvo store $ listConversations
[]
-}
initInMemoryStore :: IO InMemoryStore
initInMemoryStore = InMemoryStore <$> newTVarIO Map.empty