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

Introduce the LLM session manager classes #141

Merged
merged 41 commits into from
Jul 4, 2024
Merged
Changes from 1 commit
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
26c656b
wip: defining base session manager class methods
justin-cechmanek Apr 16, 2024
872e0fe
removes base session manager parent class
justin-cechmanek Apr 22, 2024
65177b2
adds proposed schema to sesssion manager description
justin-cechmanek Apr 22, 2024
ebc07d6
adds session manager init
justin-cechmanek Apr 25, 2024
9dbc928
wip: minimal working session manager
justin-cechmanek Apr 26, 2024
bb467c2
wip: adding full conversation history, and cleans up scoping
justin-cechmanek Apr 30, 2024
4ed6208
minor clean up
justin-cechmanek May 1, 2024
2c5c52a
wip: initial notebook demo on semantic session manager
justin-cechmanek May 1, 2024
4d0b9ce
wip: continues session manager work
justin-cechmanek May 2, 2024
ba2575a
cleans up first session manager notebook
justin-cechmanek May 2, 2024
5fda1ec
makes scope configurable on each call to fetch_context
justin-cechmanek May 3, 2024
37ae270
improves scope settings
justin-cechmanek May 4, 2024
4e432a4
adds notebook example of controling session access scope
justin-cechmanek May 4, 2024
3d404e7
formatting
justin-cechmanek May 6, 2024
c20d23e
mypy formatting
justin-cechmanek May 6, 2024
29b4a05
black formatting
justin-cechmanek May 6, 2024
e612195
bumps notebook number
justin-cechmanek May 6, 2024
45427ad
corrects method name
justin-cechmanek May 6, 2024
d9aacf9
sets an asymetric retrieval model as default vectorizer
justin-cechmanek May 7, 2024
3a117eb
Merge branch 'main' into jc/semantic-session-manager
justin-cechmanek May 8, 2024
f99967f
moves recency sorting into Redis query
justin-cechmanek May 8, 2024
5d4f34b
adds session manager notebook examples to index
justin-cechmanek May 8, 2024
dff924c
wip:refactor into multiple classes
justin-cechmanek May 15, 2024
c8b9325
Merge branch 'main' into jc/semantic-session-manager
justin-cechmanek May 15, 2024
6e8ca02
refactors session managers into multiple classes
justin-cechmanek May 27, 2024
90cfe59
adds tests for session managers
justin-cechmanek May 28, 2024
b2e80b5
removes redundant notebook
justin-cechmanek May 28, 2024
d898941
formatting
justin-cechmanek May 28, 2024
46fb9b5
formatting
justin-cechmanek May 28, 2024
58be5b7
fixes failing test
justin-cechmanek May 28, 2024
ce09632
changes user_id, session_id to user_tag, session_tag. Adds pydantic v…
justin-cechmanek May 29, 2024
d32b12e
makes system preamble fully optional, empty if not set
justin-cechmanek May 29, 2024
a5fb413
renames methods & properties to align with OpenAI and LangChain
justin-cechmanek Jun 12, 2024
c683532
changes session managers to match langchain chat history api
justin-cechmanek Jun 14, 2024
d666fad
adds messages property to match langchain
justin-cechmanek Jun 17, 2024
158eb52
adds test coverage for messages property
justin-cechmanek Jun 17, 2024
60a1a00
adds optional tool message type
justin-cechmanek Jun 28, 2024
85ba8d0
Merge branch 'main' into jc/semantic-session-manager
justin-cechmanek Jun 28, 2024
241199e
Bugfix in setting vectorizer. Uses index key_separator
justin-cechmanek Jul 2, 2024
5c5a385
updates doc strings
justin-cechmanek Jul 3, 2024
a0d0a21
empty arrary is returned when top_k=0
justin-cechmanek Jul 3, 2024
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
168 changes: 152 additions & 16 deletions redisvl/extensions/session_manager/session.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
import hashlib
from typing import Any, Dict, List, Optional, Tuple, Union
from datetime import datetime

from redis import Redis

class SessionManager():
from redisvl.index import SearchIndex
from redisvl.query import RangeQuery
from redisvl.redis.utils import array_to_buffer
from redisvl.schema.schema import IndexSchema
from redisvl.utils.vectorize import BaseVectorizer, HFTextVectorizer

class SessionManager:
_session_id: str = "any_session"
_user_id: str = "any_user"
_application_id: str = "any_app"

def __init__(self,
from_file: Optional[str] = None,
vectorizer: Vectorizer = None
name: str = "session",
prefix: Optional[str] = None,
session_id: str = None,
user_id: str = None,
application_id: str = None,
vectorizer: Optional[BaseVectorizer] = None,
redis_client: Optional[Redis] = None,
redis_url: str = "redis://localhost:6379"
):
Expand All @@ -16,7 +33,8 @@ def __init__(self,
as exchanges.

Args:
from_file Optional[str]: File to intiaialize the session index with.
name str:
prefix Optional[str]:
vectorizer Vectorizer: The vectorizer to create embeddings with.
redis_client Optional[Redis]: A Redis client instance. Defaults to
None.
Expand All @@ -28,22 +46,40 @@ def __init__(self,
constructed from the prompt & response in a single string.

"""
if not prefix:
prefix = name
if not session_id:
self._session_id = "any_session"
if not user_id:
self._user_id = "any_id"
if not application_id:
self._application_id = "any_app"

if vectorizer is None:
self._vectorizer = HFTextVectorizer(
justin-cechmanek marked this conversation as resolved.
Show resolved Hide resolved
model="sentence-transformers/all-mpnet-base-v2"
)

schema = IndexSchema.from_dict({"index": {"name": name, "prefix": prefix}})

schema.add_fields(
[
{"name": prompt_field, "type": "text"},
{"name": response_field, "type": "text"},
{"name": timestamp, "type": "numeric"},
{"name": user_id, "type": "tag"},
{"name": "prompt", "type": "text"},
{"name": "response", "type": "text"},
{"name": "timestamp", "type": "numeric"},
{"name": self._session_id, "type": "tag"},
{"name": self._user_id, "type": "tag"},
{"name": self._application_id, "type": "tag"},
{"name": "token_count", "type": "numeric"},
{
"name": combined_vector_field,
"name": "combined_vector_field",
"type": "vector",
"attrs": {
"dims": vectorizer_dims,
"dims": self._vectorizer.dims,
"datatype": "float32",
"distance_metric": "cosine",
"algorithm": "flat",
},
},
]
)
Expand All @@ -63,7 +99,7 @@ def clear(self):
pass


def fetch_context( self, prompt: str, as_text: bool = False, top_k: int = 5) -> Union[str, List[str]]:
def fetch_context(self, prompt: str, as_text: bool = False, top_k: int = 5) -> Union[str, List[str]]:
""" Searches the chat history for information semantically related to
the specified prompt.

Expand All @@ -86,7 +122,28 @@ def fetch_context( self, prompt: str, as_text: bool = False, top_k: int = 5) ->
Raises:
ValueError: If top_k is an invalid integer.
"""
pass

return_fields = [
self._session_id,
self._user_id,
self._application_id,
"prompt",
"response",
"combined_vector_field",
]


query = RangeQuery(
vector=self._vectorizer.embed(prompt),
vector_field_name="combined_vector_field",
return_fields=return_fields,
distance_threshold=0.2, #self._distance_threshold
num_results=top_k,
return_score=True
)

hits = self._index.query(query)
return hits


def conversation_history(self, as_text: bool = False) -> Union[str, List[str]]:
Expand Down Expand Up @@ -120,11 +177,11 @@ def _order_by(self, exchanges: List[str], recency: bool = True) -> List[str]:

@property
def distance_threshold(self):
return self.distance_threshold
return self._distance_threshold


def set_distance_threshold(self, threshold):
self.set_distance_threshold = threshold
self._distance_threshold = threshold


def summarize(self) -> str:
Expand All @@ -139,12 +196,91 @@ def load_previous_sessions(self):
pass


def store(self, exchange: Tuple[str, str]):
def store(self,
exchange: Tuple[str, str],
scope: str,
session_id: Union[str, int] = None,
user_id: Union[str, int] = None,
application_id: Union[str, int] = None,
):
""" Insert a prompt:response pair into the session memory. A timestamp
is associated with each exchange so that they can be later sorted
in sequential ordering after retrieval.

Args:
exchange Tuple[str, str]: The user prompt and corresponding LLM response.
exchange Tuple[str, str]: The user prompt and corresponding LLM
response.
scope str: the scope of access this exchange can be retrieved by.
must be one of {Session, User, Application}.
session_id Union[str, int]: = the session id tag to index this
exchange with. Must be provided if scope==Session.
user_id Union[str, int]: the user id tag to index this exchange
with. Must be provided if scope==User
application_id Union[str, int]: = the application id tag to
index this exchange with. Must be provided if scope==Application
"""

vector = self._vectorizer.embed(exchange[0] + exchange[1])
timestamp = int(datetime.now().timestamp())
payload = {
justin-cechmanek marked this conversation as resolved.
Show resolved Hide resolved
#TODO decide if hash id should be based on prompt, response,
# user, session, app, or some combination
"id": self.hash_input(exchange[0]+str(timestamp)),
"prompt": exchange[0],
"response": exchange[1],
"timestamp": timestamp,
"session_id": session_id or self._session_id,
"user_id": user_id or self._user_id,
"application_id": application_id or self._application_id,
"token_count": 1, #TODO get actual token count
"combined_vector_field": array_to_buffer(vector)
}
'''
{"name": "prompt", "type": "text"},
{"name": "response", "type": "text"},
{"name": "timestamp", "type": "numeric"},
{"name": session_id, "type": "tag"},
{"name": user_id, "type": "tag"},
{"name": application_id, "type": "tag"},
{"name": "token_count", "type": "numeric"},
{
"name": "combined_vector_field",
"type": "vector",
"attrs": {
"dims": self._vectorizer.dims,
"datatype": "float32",
"distance_metric": "cosine",
"algorithm": "flat",
},
}
'''
keys = self._index.load(data=[payload])
return keys


def set_preamble(self, prompt: str) -> None:
""" Add a preamble statement to the the begining of each session history
and will be included in each subsequent LLM call.
"""
self._preamble = prompt # TODO store this in Redis with asigned scope?


def timstamp_to_int(self, timestamp: datetime.timestamp) -> int:
""" Converts a datetime object into integer for storage as numeric field
in hash.
"""
pass


def int_to_timestamp(self, epoch_time: int) -> datetime.timestamp:
""" Converts a numeric date expressed in epoch time into datetime
object.
"""
pass


def hash_input(self, prompt: str):
"""Hashes the input using SHA256."""
#TODO find out if this is really necessary
return hashlib.sha256(prompt.encode("utf-8")).hexdigest()

Loading