v0.0.4 (#25)
* 🧪 asyncify tests * ✨ asyncify client * Basic Test for Page based pagination * add sync buildstep and client * add vscode DX * Added Testing for generators and updated examples * feat: example updates * readme exists now * Stylistic changes and generic message * Metamessages with other refactoring - untested * Work with unit tests * Fix Examples * MEME-78 Update Changelogs * Docstrings to client * 🧪 autogenerate sync tests * test one * add db type * sync client * add status badge * add coverage * add file * give perms * properly output coverage * split test and coverage * rename action * 🧪 autogenerate sync tests (#16) * Vector Support (#18) * Scaffold for PGVector support * Buggy crud with logic skeleton on api * Crud logic and schema definition for pgvector * Populate all routes and refactor to name Collection * vince's progress * AsyncCollection progress * Local PGVector Docker Container * client methods for sdk except document delete and update * Vector Support Passing All Test Cases * Docs Updates --------- Co-authored-by: vintro <vince@plasticlabs.ai> * Add reverse parameters for paginated routes * Address dependabot * Formatting * initial commit on honcho dspy personas * working, hit token limit and can't test dspy optimization * initial version working, need to test optimization * optimizers working, but appending any example * ready for user object (tbomk) * Revert "add test actions and coverage" * Refactor to add User and App Tables * User Object passing test cases * Update examples * DSPy Todo and documentation updates * Add is_active filtering * Add is_active filtering to the generator * Fix update user metadata * working, but weird compiler error * fixed str error in optimizer * ship --------- Co-authored-by: hyusap <paulayush@gmail.com> Co-authored-by: vintro <vince@plasticlabs.ai>
This commit is contained in:
parent
a0a10e5002
commit
5ebb8559bf
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"python.analysis.typeCheckingMode": "basic"
|
||||
}
|
||||
|
|
@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [0.0.4] — 2024-02-22
|
||||
|
||||
### Added
|
||||
|
||||
* apps table with a relationship to the users table
|
||||
* users table with a relationship to the collections and sessions tables
|
||||
* Reverse Pagination support to get recent messages, sessions, etc. more easily
|
||||
* Linting Rules
|
||||
|
||||
### Changed
|
||||
|
||||
* Get sessions method returns all sessions including inactive
|
||||
* using timestampz instead of timestamp
|
||||
|
||||
|
||||
## [0.0.3] — 2024-02-15
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -17,25 +17,26 @@ typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""}
|
|||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "3.7.1"
|
||||
version = "4.3.0"
|
||||
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"},
|
||||
{file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"},
|
||||
{file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"},
|
||||
{file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
exceptiongroup = {version = "*", markers = "python_version < \"3.11\""}
|
||||
exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""}
|
||||
idna = ">=2.8"
|
||||
sniffio = ">=1.1"
|
||||
typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"]
|
||||
test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
|
||||
trio = ["trio (<0.22)"]
|
||||
doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
|
||||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
|
||||
trio = ["trio (>=0.23)"]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
|
|
@ -123,35 +124,34 @@ test = ["pytest (>=6)"]
|
|||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.105.0"
|
||||
version = "0.109.2"
|
||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "fastapi-0.105.0-py3-none-any.whl", hash = "sha256:f19ebf6fdc82a3281d10f2cb4774bdfa90238e3b40af3525a0c09fd08ad1c480"},
|
||||
{file = "fastapi-0.105.0.tar.gz", hash = "sha256:4d12838819aa52af244580675825e750ad67c9df4614f557a769606af902cf22"},
|
||||
{file = "fastapi-0.109.2-py3-none-any.whl", hash = "sha256:2c9bab24667293b501cad8dd388c05240c850b58ec5876ee3283c47d6e1e3a4d"},
|
||||
{file = "fastapi-0.109.2.tar.gz", hash = "sha256:f3817eac96fe4f65a2ebb4baa000f394e55f5fccdaf7f75250804bc58f354f73"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = ">=3.7.1,<4.0.0"
|
||||
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
|
||||
starlette = ">=0.27.0,<0.28.0"
|
||||
starlette = ">=0.36.3,<0.37.0"
|
||||
typing-extensions = ">=4.8.0"
|
||||
|
||||
[package.extras]
|
||||
all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
||||
all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi-pagination"
|
||||
version = "0.12.15"
|
||||
version = "0.12.16"
|
||||
description = "FastAPI pagination"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8,<4.0"
|
||||
files = [
|
||||
{file = "fastapi_pagination-0.12.15-py3-none-any.whl", hash = "sha256:bcfea8622b48135ef759b926d9d09fa8e16bc8adab26ec2b65d1647e72d39988"},
|
||||
{file = "fastapi_pagination-0.12.15.tar.gz", hash = "sha256:a7e5e48cd9d183f29532455a1689dfac575877b7ff10d112ddb56cb3d047a457"},
|
||||
{file = "fastapi_pagination-0.12.16-py3-none-any.whl", hash = "sha256:1179edea6c8d3b6b70d3f373047470b08a948bfef817ff8e722d46969f87998c"},
|
||||
{file = "fastapi_pagination-0.12.16.tar.gz", hash = "sha256:3c74d77d42451518e9d85aa1c3633b725f42d9746d68d1e9267f6c0493750497"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
|
@ -340,14 +340,14 @@ testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)",
|
|||
|
||||
[[package]]
|
||||
name = "limits"
|
||||
version = "3.8.0"
|
||||
version = "3.9.0"
|
||||
description = "Rate limiting utilities"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "limits-3.8.0-py3-none-any.whl", hash = "sha256:6e3c75712359dfaea28bee23832bd814bbe66a42c92bbd848154dfba0d4c4503"},
|
||||
{file = "limits-3.8.0.tar.gz", hash = "sha256:7dd4955dec3c7a219be04e661251ae243a48050e84053bf68b31dd07890f28c2"},
|
||||
{file = "limits-3.9.0-py3-none-any.whl", hash = "sha256:6dce07d1a4d7bd3361d36f59f3f43c4f39675001daeeae2617c3be42d718daa8"},
|
||||
{file = "limits-3.9.0.tar.gz", hash = "sha256:7b44aa4d05c539276928372681190136914958cccbb99c30ecc5df72a179661a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
|
@ -785,14 +785,14 @@ sqlcipher = ["sqlcipher3_binary"]
|
|||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.27.0"
|
||||
version = "0.36.3"
|
||||
description = "The little ASGI library that shines."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"},
|
||||
{file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"},
|
||||
{file = "starlette-0.36.3-py3-none-any.whl", hash = "sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044"},
|
||||
{file = "starlette-0.36.3.tar.gz", hash = "sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
|
@ -800,7 +800,7 @@ anyio = ">=3.4.0,<5"
|
|||
typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
|
||||
|
||||
[package.extras]
|
||||
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"]
|
||||
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
|
|
@ -954,4 +954,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.8"
|
||||
content-hash = "90a0874f29e706994647a141418ed4eca5bd621518396d525d27039ad586e4bc"
|
||||
content-hash = "49ec8fef5f21cb5bf2a8bbd007f016bd5bd88f8bdf604f3a820c59c07f984060"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
[tool.poetry]
|
||||
name = "honcho"
|
||||
version = "0.0.3"
|
||||
version = "0.0.4"
|
||||
description = "Honcho Server"
|
||||
authors = ["Plastic Labs <hello@plasticlabs.ai>"]
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
fastapi = "^0.105.0"
|
||||
fastapi = "^0.109.0"
|
||||
uvicorn = "^0.24.0.post1"
|
||||
python-dotenv = "^1.0.0"
|
||||
sqlalchemy = "^2.0.25"
|
||||
|
|
@ -17,6 +17,23 @@ fastapi-pagination = "^0.12.14"
|
|||
pgvector = "^0.2.5"
|
||||
openai = "^1.12.0"
|
||||
|
||||
[tool.ruff.lint]
|
||||
# from https://docs.astral.sh/ruff/linter/#rule-selection example
|
||||
select = [
|
||||
# pycodestyle
|
||||
"E",
|
||||
# Pyflakes
|
||||
"F",
|
||||
# pyupgrade
|
||||
"UP",
|
||||
# flake8-bugbear
|
||||
"B",
|
||||
# flake8-simplify
|
||||
"SIM",
|
||||
# isort
|
||||
"I",
|
||||
]
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
|
|
|
|||
540
api/src/crud.py
540
api/src/crud.py
|
|
@ -1,45 +1,201 @@
|
|||
import uuid
|
||||
import datetime
|
||||
import uuid
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
from sqlalchemy import select, Select
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import Select, select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from . import models, schemas
|
||||
|
||||
openai_client = OpenAI()
|
||||
|
||||
def get_session(db: Session, app_id: str, session_id: uuid.UUID, user_id: Optional[str] = None) -> Optional[models.Session]:
|
||||
stmt = select(models.Session).where(models.Session.app_id == app_id).where(models.Session.id == session_id)
|
||||
########################################################
|
||||
# app methods
|
||||
########################################################
|
||||
|
||||
|
||||
def get_app(db: Session, app_id: uuid.UUID) -> Optional[models.App]:
|
||||
stmt = select(models.App).where(models.App.id == app_id)
|
||||
app = db.scalars(stmt).one_or_none()
|
||||
return app
|
||||
|
||||
|
||||
def get_app_by_name(db: Session, name: str) -> Optional[models.App]:
|
||||
stmt = select(models.App).where(models.App.name == name)
|
||||
app = db.scalars(stmt).one_or_none()
|
||||
return app
|
||||
|
||||
|
||||
# def get_apps(db: Session) -> Sequence[models.App]:
|
||||
# return db.query(models.App).all()
|
||||
|
||||
|
||||
def create_app(db: Session, app: schemas.AppCreate) -> models.App:
|
||||
honcho_app = models.App(name=app.name, h_metadata=app.metadata)
|
||||
db.add(honcho_app)
|
||||
db.commit()
|
||||
db.refresh(honcho_app)
|
||||
return honcho_app
|
||||
|
||||
|
||||
def update_app(db: Session, app_id: uuid.UUID, app: schemas.AppUpdate) -> models.App:
|
||||
honcho_app = get_app(db, app_id)
|
||||
if honcho_app is None:
|
||||
raise ValueError("App not found")
|
||||
if app.name is not None:
|
||||
honcho_app.content = app.name
|
||||
if app.metadata is not None:
|
||||
honcho_app.h_metadata = app.metadata
|
||||
|
||||
db.commit()
|
||||
db.refresh(honcho_app)
|
||||
return honcho_app
|
||||
|
||||
|
||||
# def delete_app(db: Session, app_id: uuid.UUID) -> bool:
|
||||
# existing_app = get_app(db, app_id)
|
||||
# if existing_app is None:
|
||||
# return False
|
||||
# db.delete(existing_app)
|
||||
# db.commit()
|
||||
# return True
|
||||
|
||||
|
||||
########################################################
|
||||
# user methods
|
||||
########################################################
|
||||
|
||||
|
||||
def create_user(
|
||||
db: Session, app_id: uuid.UUID, user: schemas.UserCreate
|
||||
) -> models.User:
|
||||
honcho_user = models.User(
|
||||
app_id=app_id,
|
||||
name=user.name,
|
||||
h_metadata=user.metadata,
|
||||
)
|
||||
db.add(honcho_user)
|
||||
db.commit()
|
||||
db.refresh(honcho_user)
|
||||
return honcho_user
|
||||
|
||||
|
||||
def get_user(
|
||||
db: Session, app_id: uuid.UUID, user_id: uuid.UUID
|
||||
) -> Optional[models.User]:
|
||||
stmt = (
|
||||
select(models.User)
|
||||
.where(models.User.app_id == app_id)
|
||||
.where(models.User.id == user_id)
|
||||
)
|
||||
user = db.scalars(stmt).one_or_none()
|
||||
return user
|
||||
|
||||
|
||||
def get_user_by_name(
|
||||
db: Session, app_id: uuid.UUID, name: str
|
||||
) -> Optional[models.User]:
|
||||
stmt = (
|
||||
select(models.User)
|
||||
.where(models.User.app_id == app_id)
|
||||
.where(models.User.name == name)
|
||||
)
|
||||
user = db.scalars(stmt).one_or_none()
|
||||
return user
|
||||
|
||||
|
||||
def get_users(db: Session, app_id: uuid.UUID, reverse: bool = False) -> Select:
|
||||
stmt = select(models.User).where(models.User.app_id == app_id)
|
||||
if reverse:
|
||||
stmt = stmt.order_by(models.User.created_at.desc())
|
||||
else:
|
||||
stmt = stmt.order_by(models.User.created_at)
|
||||
|
||||
return stmt
|
||||
|
||||
|
||||
def update_user(
|
||||
db: Session, app_id: uuid.UUID, user_id: uuid.UUID, user: schemas.UserUpdate
|
||||
) -> models.User:
|
||||
honcho_user = get_user(db, app_id, user_id)
|
||||
if honcho_user is None:
|
||||
raise ValueError("User not found")
|
||||
if user.name is not None:
|
||||
honcho_user.content = user.name
|
||||
if user.metadata is not None:
|
||||
honcho_user.h_metadata = user.metadata
|
||||
|
||||
db.commit()
|
||||
db.refresh(honcho_user)
|
||||
return honcho_user
|
||||
|
||||
|
||||
# def delete_user(db: Session, app_id: uuid.UUID, user_id: uuid.UUID) -> bool:
|
||||
# existing_user = get_user(db, app_id, user_id)
|
||||
# if existing_user is None:
|
||||
# return False
|
||||
# db.delete(existing_user)
|
||||
# db.commit()
|
||||
# return True
|
||||
|
||||
########################################################
|
||||
# session methods
|
||||
########################################################
|
||||
|
||||
|
||||
def get_session(
|
||||
db: Session,
|
||||
app_id: uuid.UUID,
|
||||
session_id: uuid.UUID,
|
||||
user_id: Optional[uuid.UUID] = None,
|
||||
) -> Optional[models.Session]:
|
||||
stmt = (
|
||||
select(models.Session)
|
||||
.join(models.User, models.User.id == models.Session.user_id)
|
||||
.where(models.User.app_id == app_id)
|
||||
.where(models.Session.id == session_id)
|
||||
)
|
||||
if user_id is not None:
|
||||
stmt = stmt.where(models.Session.user_id == user_id)
|
||||
session = db.scalars(stmt).one_or_none()
|
||||
return session
|
||||
|
||||
|
||||
def get_sessions(
|
||||
db: Session, app_id: str, user_id: str, location_id: str | None = None
|
||||
db: Session,
|
||||
app_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
location_id: Optional[str] = None,
|
||||
reverse: Optional[bool] = False,
|
||||
is_active: Optional[bool] = False,
|
||||
) -> Select:
|
||||
stmt = (
|
||||
select(models.Session)
|
||||
.where(models.Session.app_id == app_id)
|
||||
.join(models.User, models.User.id == models.Session.user_id)
|
||||
.where(models.User.app_id == app_id)
|
||||
.where(models.Session.user_id == user_id)
|
||||
.where(models.Session.is_active.is_(True))
|
||||
.order_by(models.Session.created_at)
|
||||
)
|
||||
|
||||
if is_active:
|
||||
stmt = stmt.where(models.Session.is_active.is_(True))
|
||||
|
||||
if reverse:
|
||||
stmt = stmt.order_by(models.Session.created_at.desc())
|
||||
else:
|
||||
stmt = stmt.order_by(models.Session.created_at)
|
||||
|
||||
if location_id is not None:
|
||||
stmt = stmt.where(models.Session.location_id == location_id)
|
||||
|
||||
return stmt
|
||||
|
||||
|
||||
def create_session(
|
||||
db: Session, session: schemas.SessionCreate, app_id: str, user_id: str
|
||||
db: Session, session: schemas.SessionCreate, app_id: uuid.UUID, user_id: uuid.UUID
|
||||
) -> models.Session:
|
||||
honcho_session = models.Session(
|
||||
app_id=app_id,
|
||||
user_id=user_id,
|
||||
location_id=session.location_id,
|
||||
h_metadata=session.metadata,
|
||||
|
|
@ -51,22 +207,34 @@ def create_session(
|
|||
|
||||
|
||||
def update_session(
|
||||
db: Session, session: schemas.SessionUpdate, app_id: str, user_id: str, session_id: uuid.UUID
|
||||
db: Session,
|
||||
session: schemas.SessionUpdate,
|
||||
app_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
session_id: uuid.UUID,
|
||||
) -> bool:
|
||||
honcho_session = get_session(db, app_id=app_id, session_id=session_id, user_id=user_id)
|
||||
honcho_session = get_session(
|
||||
db, app_id=app_id, session_id=session_id, user_id=user_id
|
||||
)
|
||||
if honcho_session is None:
|
||||
raise ValueError("Session not found or does not belong to user")
|
||||
if session.metadata is not None: # Need to explicitly be there won't make it empty by default
|
||||
if (
|
||||
session.metadata is not None
|
||||
): # Need to explicitly be there won't make it empty by default
|
||||
honcho_session.h_metadata = session.metadata
|
||||
db.commit()
|
||||
db.refresh(honcho_session)
|
||||
return honcho_session
|
||||
|
||||
def delete_session(db: Session, app_id: str, user_id: str, session_id: uuid.UUID) -> bool:
|
||||
|
||||
def delete_session(
|
||||
db: Session, app_id: uuid.UUID, user_id: uuid.UUID, session_id: uuid.UUID
|
||||
) -> bool:
|
||||
stmt = (
|
||||
select(models.Session)
|
||||
.join(models.User, models.User.id == models.Session.user_id)
|
||||
.where(models.Session.id == session_id)
|
||||
.where(models.Session.app_id == app_id)
|
||||
.where(models.User.app_id == app_id)
|
||||
.where(models.Session.user_id == user_id)
|
||||
)
|
||||
honcho_session = db.scalars(stmt).one_or_none()
|
||||
|
|
@ -76,10 +244,22 @@ def delete_session(db: Session, app_id: str, user_id: str, session_id: uuid.UUID
|
|||
db.commit()
|
||||
return True
|
||||
|
||||
|
||||
########################################################
|
||||
# Message Methods
|
||||
########################################################
|
||||
|
||||
|
||||
def create_message(
|
||||
db: Session, message: schemas.MessageCreate, app_id: str, user_id: str, session_id: uuid.UUID
|
||||
db: Session,
|
||||
message: schemas.MessageCreate,
|
||||
app_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
session_id: uuid.UUID,
|
||||
) -> models.Message:
|
||||
honcho_session = get_session(db, app_id=app_id, session_id=session_id, user_id=user_id)
|
||||
honcho_session = get_session(
|
||||
db, app_id=app_id, session_id=session_id, user_id=user_id
|
||||
)
|
||||
if honcho_session is None:
|
||||
raise ValueError("Session not found or does not belong to user")
|
||||
|
||||
|
|
@ -93,77 +273,71 @@ def create_message(
|
|||
db.refresh(honcho_message)
|
||||
return honcho_message
|
||||
|
||||
|
||||
def get_messages(
|
||||
db: Session, app_id: str, user_id: str, session_id: uuid.UUID
|
||||
db: Session,
|
||||
app_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
session_id: uuid.UUID,
|
||||
reverse: Optional[bool] = False,
|
||||
) -> Select:
|
||||
stmt = (
|
||||
select(models.Message)
|
||||
.join(models.Session, models.Session.id == models.Message.session_id)
|
||||
.where(models.Session.app_id == app_id)
|
||||
.where(models.Session.user_id == user_id)
|
||||
.join(models.User, models.User.id == models.Session.user_id)
|
||||
.join(models.App, models.App.id == models.User.app_id)
|
||||
.where(models.App.id == app_id)
|
||||
.where(models.User.id == user_id)
|
||||
.where(models.Message.session_id == session_id)
|
||||
.order_by(models.Message.created_at)
|
||||
)
|
||||
|
||||
if reverse:
|
||||
stmt = stmt.order_by(models.Message.created_at.desc())
|
||||
else:
|
||||
stmt = stmt.order_by(models.Message.created_at)
|
||||
|
||||
return stmt
|
||||
|
||||
|
||||
def get_message(
|
||||
db: Session, app_id: str, user_id: str, session_id: uuid.UUID, message_id: uuid.UUID
|
||||
db: Session,
|
||||
app_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
session_id: uuid.UUID,
|
||||
message_id: uuid.UUID,
|
||||
) -> Optional[models.Message]:
|
||||
stmt = (
|
||||
select(models.Message)
|
||||
.join(models.Session, models.Session.id == models.Message.session_id)
|
||||
.where(models.Session.app_id == app_id)
|
||||
.where(models.Session.user_id == user_id)
|
||||
.join(models.User, models.User.id == models.Session.user_id)
|
||||
.join(models.App, models.App.id == models.User.app_id)
|
||||
.where(models.App.id == app_id)
|
||||
.where(models.User.id == user_id)
|
||||
.where(models.Message.session_id == session_id)
|
||||
.where(models.Message.id == message_id)
|
||||
|
||||
)
|
||||
return db.scalars(stmt).one_or_none()
|
||||
|
||||
|
||||
########################################################
|
||||
# metamessage methods
|
||||
########################################################
|
||||
|
||||
def get_metamessages(db: Session, app_id: str, user_id: str, session_id: uuid.UUID, message_id: Optional[uuid.UUID], metamessage_type: Optional[str] = None) -> Select:
|
||||
stmt = (
|
||||
select(models.Metamessage)
|
||||
.join(models.Message, models.Message.id == models.Metamessage.message_id)
|
||||
.join(models.Session, models.Message.session_id == models.Session.id)
|
||||
.where(models.Session.app_id == app_id)
|
||||
.where(models.Session.user_id == user_id)
|
||||
.where(models.Message.session_id == session_id)
|
||||
.order_by(models.Metamessage.created_at)
|
||||
)
|
||||
if message_id is not None:
|
||||
stmt = stmt.where(models.Metamessage.message_id == message_id)
|
||||
if metamessage_type is not None:
|
||||
stmt = stmt.where(models.Metamessage.metamessage_type == metamessage_type)
|
||||
return stmt
|
||||
|
||||
def get_metamessage(
|
||||
db: Session, app_id: str, user_id: str, session_id: uuid.UUID, message_id: uuid.UUID, metamessage_id: uuid.UUID
|
||||
) -> Optional[models.Metamessage]:
|
||||
stmt = (
|
||||
select(models.Metamessage)
|
||||
.join(models.Message, models.Message.id == models.Metamessage.message_id)
|
||||
.join(models.Session, models.Message.session_id == models.Session.id)
|
||||
.where(models.Session.app_id == app_id)
|
||||
.where(models.Session.user_id == user_id)
|
||||
.where(models.Message.session_id == session_id)
|
||||
.where(models.Metamessage.message_id == message_id)
|
||||
.where(models.Metamessage.id == metamessage_id)
|
||||
|
||||
)
|
||||
return db.scalars(stmt).one_or_none()
|
||||
|
||||
def create_metamessage(
|
||||
db: Session,
|
||||
metamessage: schemas.MetamessageCreate,
|
||||
app_id: str,
|
||||
user_id: str,
|
||||
app_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
session_id: uuid.UUID,
|
||||
):
|
||||
message = get_message(db, app_id=app_id, session_id=session_id, user_id=user_id, message_id=metamessage.message_id)
|
||||
message = get_message(
|
||||
db,
|
||||
app_id=app_id,
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
message_id=metamessage.message_id,
|
||||
)
|
||||
if message is None:
|
||||
raise ValueError("Session not found or does not belong to user")
|
||||
|
||||
|
|
@ -178,47 +352,125 @@ def create_metamessage(
|
|||
db.refresh(honcho_metamessage)
|
||||
return honcho_metamessage
|
||||
|
||||
|
||||
def get_metamessages(
|
||||
db: Session,
|
||||
app_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
session_id: uuid.UUID,
|
||||
message_id: Optional[uuid.UUID],
|
||||
metamessage_type: Optional[str] = None,
|
||||
reverse: Optional[bool] = False,
|
||||
) -> Select:
|
||||
stmt = (
|
||||
select(models.Metamessage)
|
||||
.join(models.Message, models.Message.id == models.Metamessage.message_id)
|
||||
.join(models.Session, models.Message.session_id == models.Session.id)
|
||||
.join(models.User, models.User.id == models.Session.user_id)
|
||||
.join(models.App, models.App.id == models.User.app_id)
|
||||
.where(models.App.id == app_id)
|
||||
.where(models.User.id == user_id)
|
||||
.where(models.Message.session_id == session_id)
|
||||
)
|
||||
|
||||
if message_id is not None:
|
||||
stmt = stmt.where(models.Metamessage.message_id == message_id)
|
||||
|
||||
if metamessage_type is not None:
|
||||
stmt = stmt.where(models.Metamessage.metamessage_type == metamessage_type)
|
||||
|
||||
if reverse:
|
||||
stmt = stmt.order_by(models.Metamessage.created_at.desc())
|
||||
else:
|
||||
stmt = stmt.order_by(models.Metamessage.created_at)
|
||||
|
||||
return stmt
|
||||
|
||||
|
||||
def get_metamessage(
|
||||
db: Session,
|
||||
app_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
session_id: uuid.UUID,
|
||||
message_id: uuid.UUID,
|
||||
metamessage_id: uuid.UUID,
|
||||
) -> Optional[models.Metamessage]:
|
||||
stmt = (
|
||||
select(models.Metamessage)
|
||||
.join(models.Message, models.Message.id == models.Metamessage.message_id)
|
||||
.join(models.Session, models.Message.session_id == models.Session.id)
|
||||
.join(models.User, models.User.id == models.Session.user_id)
|
||||
.join(models.App, models.App.id == models.User.app_id)
|
||||
.where(models.App.id == app_id)
|
||||
.where(models.User.id == user_id)
|
||||
.where(models.Message.session_id == session_id)
|
||||
.where(models.Metamessage.message_id == message_id)
|
||||
.where(models.Metamessage.id == metamessage_id)
|
||||
)
|
||||
return db.scalars(stmt).one_or_none()
|
||||
|
||||
|
||||
########################################################
|
||||
# collection methods
|
||||
########################################################
|
||||
|
||||
# Should be very similar to the session methods
|
||||
|
||||
def get_collections(db: Session, app_id: str, user_id: str) -> Select:
|
||||
|
||||
def get_collections(
|
||||
db: Session, app_id: uuid.UUID, user_id: uuid.UUID, reverse: Optional[bool] = False
|
||||
) -> Select:
|
||||
"""Get a distinct list of the names of collections associated with a user"""
|
||||
stmt = (
|
||||
select(models.Collection)
|
||||
.where(models.Collection.app_id == app_id)
|
||||
.where(models.Collection.user_id == user_id)
|
||||
.order_by(models.Collection.created_at)
|
||||
.join(models.User, models.User.id == models.Collection.user_id)
|
||||
.where(models.User.app_id == app_id)
|
||||
.where(models.User.id == user_id)
|
||||
)
|
||||
|
||||
if reverse:
|
||||
stmt = stmt.order_by(models.Collection.created_at.desc())
|
||||
else:
|
||||
stmt = stmt.order_by(models.Collection.created_at)
|
||||
|
||||
return stmt
|
||||
|
||||
def get_collection_by_id(db: Session, app_id: str, user_id: str, collection_id: uuid.UUID) -> Optional[models.Collection]:
|
||||
stmt = (
|
||||
|
||||
def get_collection_by_id(
|
||||
db: Session, app_id: uuid.UUID, user_id: uuid.UUID, collection_id: uuid.UUID
|
||||
) -> Optional[models.Collection]:
|
||||
stmt = (
|
||||
select(models.Collection)
|
||||
.where(models.Collection.app_id == app_id)
|
||||
.where(models.Collection.user_id == user_id)
|
||||
.join(models.User, models.User.id == models.Collection.user_id)
|
||||
.where(models.User.app_id == app_id)
|
||||
.where(models.User.id == user_id)
|
||||
.where(models.Collection.id == collection_id)
|
||||
)
|
||||
collection = db.scalars(stmt).one_or_none()
|
||||
return collection
|
||||
|
||||
def get_collection_by_name(db: Session, app_id: str, user_id: str, name: str) -> Optional[models.Collection]:
|
||||
stmt = (
|
||||
|
||||
def get_collection_by_name(
|
||||
db: Session, app_id: uuid.UUID, user_id: uuid.UUID, name: str
|
||||
) -> Optional[models.Collection]:
|
||||
stmt = (
|
||||
select(models.Collection)
|
||||
.where(models.Collection.app_id == app_id)
|
||||
.where(models.Collection.user_id == user_id)
|
||||
.join(models.User, models.User.id == models.Collection.user_id)
|
||||
.where(models.User.app_id == app_id)
|
||||
.where(models.User.id == user_id)
|
||||
.where(models.Collection.name == name)
|
||||
)
|
||||
collection = db.scalars(stmt).one_or_none()
|
||||
return collection
|
||||
|
||||
|
||||
def create_collection(
|
||||
db: Session, collection: schemas.CollectionCreate, app_id: str, user_id: str
|
||||
db: Session,
|
||||
collection: schemas.CollectionCreate,
|
||||
app_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
) -> models.Collection:
|
||||
honcho_collection = models.Collection(
|
||||
app_id=app_id,
|
||||
user_id=user_id,
|
||||
name=collection.name,
|
||||
)
|
||||
|
|
@ -231,10 +483,17 @@ def create_collection(
|
|||
db.refresh(honcho_collection)
|
||||
return honcho_collection
|
||||
|
||||
|
||||
def update_collection(
|
||||
db: Session, collection: schemas.CollectionUpdate, app_id: str, user_id: str, collection_id: uuid.UUID
|
||||
db: Session,
|
||||
collection: schemas.CollectionUpdate,
|
||||
app_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
collection_id: uuid.UUID,
|
||||
) -> models.Collection:
|
||||
honcho_collection = get_collection_by_id(db, app_id=app_id, user_id=user_id, collection_id=collection_id)
|
||||
honcho_collection = get_collection_by_id(
|
||||
db, app_id=app_id, user_id=user_id, collection_id=collection_id
|
||||
)
|
||||
if honcho_collection is None:
|
||||
raise ValueError("collection not found or does not belong to user")
|
||||
try:
|
||||
|
|
@ -246,8 +505,9 @@ def update_collection(
|
|||
db.refresh(honcho_collection)
|
||||
return honcho_collection
|
||||
|
||||
|
||||
def delete_collection(
|
||||
db: Session, app_id: str, user_id: str, collection_id: uuid.UUID
|
||||
db: Session, app_id: uuid.UUID, user_id: uuid.UUID, collection_id: uuid.UUID
|
||||
) -> bool:
|
||||
"""
|
||||
Delete a Collection and all documents associated with it. Takes advantage of
|
||||
|
|
@ -255,9 +515,10 @@ def delete_collection(
|
|||
"""
|
||||
stmt = (
|
||||
select(models.Collection)
|
||||
.join(models.User, models.User.id == models.Collection.user_id)
|
||||
.where(models.User.app_id == app_id)
|
||||
.where(models.User.id == user_id)
|
||||
.where(models.Collection.id == collection_id)
|
||||
.where(models.Collection.app_id == app_id)
|
||||
.where(models.Collection.user_id == user_id)
|
||||
)
|
||||
honcho_collection = db.scalars(stmt).one_or_none()
|
||||
if honcho_collection is None:
|
||||
|
|
@ -266,33 +527,51 @@ def delete_collection(
|
|||
db.commit()
|
||||
return True
|
||||
|
||||
|
||||
########################################################
|
||||
# document methods
|
||||
########################################################
|
||||
|
||||
# Should be similar to the messages methods outside of query
|
||||
|
||||
|
||||
def get_documents(
|
||||
db: Session, app_id: str, user_id: str, collection_id: uuid.UUID
|
||||
db: Session,
|
||||
app_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
collection_id: uuid.UUID,
|
||||
reverse: Optional[bool] = False,
|
||||
) -> Select:
|
||||
stmt = (
|
||||
select(models.Document)
|
||||
.join(models.Collection, models.Collection.id == models.Document.collection_id)
|
||||
.where(models.Collection.app_id == app_id)
|
||||
.where(models.Collection.user_id == user_id)
|
||||
.join(models.User, models.User.id == models.Collection.user_id)
|
||||
.where(models.User.app_id == app_id)
|
||||
.where(models.User.id == user_id)
|
||||
.where(models.Document.collection_id == collection_id)
|
||||
.order_by(models.Document.created_at)
|
||||
)
|
||||
|
||||
if reverse:
|
||||
stmt = stmt.order_by(models.Document.created_at.desc())
|
||||
else:
|
||||
stmt = stmt.order_by(models.Document.created_at)
|
||||
|
||||
return stmt
|
||||
|
||||
|
||||
def get_document(
|
||||
db: Session, app_id: str, user_id: str, collection_id: uuid.UUID, document_id: uuid.UUID
|
||||
db: Session,
|
||||
app_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
collection_id: uuid.UUID,
|
||||
document_id: uuid.UUID,
|
||||
) -> Optional[models.Document]:
|
||||
stmt = (
|
||||
stmt = (
|
||||
select(models.Document)
|
||||
.join(models.Collection, models.Collection.id == models.Document.collection_id)
|
||||
.where(models.Collection.app_id == app_id)
|
||||
.where(models.Collection.user_id == user_id)
|
||||
.join(models.User, models.User.id == models.Collection.user_id)
|
||||
.where(models.User.app_id == app_id)
|
||||
.where(models.User.id == user_id)
|
||||
.where(models.Document.collection_id == collection_id)
|
||||
.where(models.Document.id == document_id)
|
||||
)
|
||||
|
|
@ -301,36 +580,49 @@ def get_document(
|
|||
return document
|
||||
|
||||
|
||||
def query_documents(db: Session, app_id: str, user_id: str, collection_id: uuid.UUID, query: str, top_k: int = 5) -> Sequence[models.Document]:
|
||||
def query_documents(
|
||||
db: Session,
|
||||
app_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
collection_id: uuid.UUID,
|
||||
query: str,
|
||||
top_k: int = 5,
|
||||
) -> Sequence[models.Document]:
|
||||
response = openai_client.embeddings.create(
|
||||
input=query,
|
||||
model="text-embedding-3-small"
|
||||
input=query, model="text-embedding-3-small"
|
||||
)
|
||||
embedding_query = response.data[0].embedding
|
||||
stmt = (
|
||||
select(models.Document)
|
||||
.join(models.Collection, models.Collection.id == models.Document.collection_id)
|
||||
.where(models.Collection.app_id == app_id)
|
||||
.where(models.Collection.user_id == user_id)
|
||||
.where(models.Document.collection_id == collection_id)
|
||||
.order_by(models.Document.embedding.cosine_distance(embedding_query))
|
||||
.limit(top_k)
|
||||
)
|
||||
select(models.Document)
|
||||
.join(models.Collection, models.Collection.id == models.Document.collection_id)
|
||||
.join(models.User, models.User.id == models.Collection.user_id)
|
||||
.where(models.User.app_id == app_id)
|
||||
.where(models.User.id == user_id)
|
||||
.where(models.Document.collection_id == collection_id)
|
||||
.order_by(models.Document.embedding.cosine_distance(embedding_query))
|
||||
.limit(top_k)
|
||||
)
|
||||
# if metadata is not None:
|
||||
# stmt = stmt.where(models.Document.h_metadata.contains(metadata))
|
||||
# stmt = stmt.where(models.Document.h_metadata.contains(metadata))
|
||||
return db.scalars(stmt).all()
|
||||
|
||||
|
||||
def create_document(
|
||||
db: Session, document: schemas.DocumentCreate, app_id: str, user_id: str, collection_id: uuid.UUID
|
||||
db: Session,
|
||||
document: schemas.DocumentCreate,
|
||||
app_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
collection_id: uuid.UUID,
|
||||
) -> models.Document:
|
||||
"""Embed a message as a vector and create a document"""
|
||||
collection = get_collection_by_id(db, app_id=app_id, collection_id=collection_id, user_id=user_id)
|
||||
collection = get_collection_by_id(
|
||||
db, app_id=app_id, collection_id=collection_id, user_id=user_id
|
||||
)
|
||||
if collection is None:
|
||||
raise ValueError("Session not found or does not belong to user")
|
||||
|
||||
response = openai_client.embeddings.create(
|
||||
input=document.content,
|
||||
model="text-embedding-3-small"
|
||||
input=document.content, model="text-embedding-3-small"
|
||||
)
|
||||
|
||||
embedding = response.data[0].embedding
|
||||
|
|
@ -339,28 +631,39 @@ def create_document(
|
|||
collection_id=collection_id,
|
||||
content=document.content,
|
||||
h_metadata=document.metadata,
|
||||
embedding=embedding
|
||||
embedding=embedding,
|
||||
)
|
||||
db.add(honcho_document)
|
||||
db.commit()
|
||||
db.refresh(honcho_document)
|
||||
return honcho_document
|
||||
|
||||
|
||||
def update_document(
|
||||
db: Session, document: schemas.DocumentUpdate, app_id: str, user_id: str, collection_id: uuid.UUID, document_id: uuid.UUID
|
||||
db: Session,
|
||||
document: schemas.DocumentUpdate,
|
||||
app_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
collection_id: uuid.UUID,
|
||||
document_id: uuid.UUID,
|
||||
) -> bool:
|
||||
honcho_document = get_document(db, app_id=app_id, collection_id=collection_id, user_id=user_id, document_id=document_id)
|
||||
honcho_document = get_document(
|
||||
db,
|
||||
app_id=app_id,
|
||||
collection_id=collection_id,
|
||||
user_id=user_id,
|
||||
document_id=document_id,
|
||||
)
|
||||
if honcho_document is None:
|
||||
raise ValueError("Session not found or does not belong to user")
|
||||
if document.content is not None:
|
||||
honcho_document.content = document.content
|
||||
response = openai_client.embeddings.create(
|
||||
input=document.content,
|
||||
model="text-embedding-3-small"
|
||||
)
|
||||
input=document.content, model="text-embedding-3-small"
|
||||
)
|
||||
embedding = response.data[0].embedding
|
||||
honcho_document.embedding = embedding
|
||||
honcho_document.created_at = datetime.datetime.now()
|
||||
honcho_document.created_at = datetime.datetime.utcnow()
|
||||
|
||||
if document.metadata is not None:
|
||||
honcho_document.h_metadata = document.metadata
|
||||
|
|
@ -368,12 +671,20 @@ def update_document(
|
|||
db.refresh(honcho_document)
|
||||
return honcho_document
|
||||
|
||||
def delete_document(db: Session, app_id: str, user_id: str, collection_id: uuid.UUID, document_id: uuid.UUID) -> bool:
|
||||
|
||||
def delete_document(
|
||||
db: Session,
|
||||
app_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
collection_id: uuid.UUID,
|
||||
document_id: uuid.UUID,
|
||||
) -> bool:
|
||||
stmt = (
|
||||
select(models.Document)
|
||||
.join(models.Collection, models.Collection.id == models.Document.collection_id)
|
||||
.where(models.Collection.app_id == app_id)
|
||||
.where(models.Collection.user_id == user_id)
|
||||
.join(models.User, models.User.id == models.Collection.user_id)
|
||||
.where(models.User.app_id == app_id)
|
||||
.where(models.User.id == user_id)
|
||||
.where(models.Document.collection_id == collection_id)
|
||||
.where(models.Document.id == document_id)
|
||||
)
|
||||
|
|
@ -383,4 +694,3 @@ def delete_document(db: Session, app_id: str, user_id: str, collection_id: uuid.
|
|||
db.delete(document)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
|
|
|
|||
752
api/src/main.py
752
api/src/main.py
File diff suppressed because it is too large
Load Diff
|
|
@ -4,7 +4,15 @@ import uuid
|
|||
|
||||
from dotenv import load_dotenv
|
||||
from pgvector.sqlalchemy import Vector
|
||||
from sqlalchemy import JSON, Column, ForeignKey, String, UniqueConstraint, Uuid
|
||||
from sqlalchemy import (
|
||||
JSON,
|
||||
Column,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
String,
|
||||
UniqueConstraint,
|
||||
Uuid,
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
|
|
@ -12,70 +20,133 @@ from .db import Base
|
|||
|
||||
load_dotenv()
|
||||
|
||||
DATABASE_TYPE = os.getenv("DATABASE_TYPE", 'postgres')
|
||||
DATABASE_TYPE = os.getenv("DATABASE_TYPE", "postgres")
|
||||
|
||||
ColumnType = JSONB if DATABASE_TYPE == "postgres" else JSON
|
||||
|
||||
|
||||
class App(Base):
|
||||
__tablename__ = "apps"
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
primary_key=True, index=True, default=uuid.uuid4
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(512), index=True, unique=True)
|
||||
users = relationship("User", back_populates="app")
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=datetime.datetime.utcnow
|
||||
)
|
||||
h_metadata: Mapped[dict] = mapped_column("metadata", ColumnType, default={})
|
||||
# Add any additional fields for an app here
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
primary_key=True, index=True, default=uuid.uuid4
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(512), index=True)
|
||||
h_metadata: Mapped[dict] = mapped_column("metadata", ColumnType, default={})
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=datetime.datetime.utcnow
|
||||
)
|
||||
app_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("apps.id"), index=True)
|
||||
app = relationship("App", back_populates="users")
|
||||
sessions = relationship("Session", back_populates="user")
|
||||
collections = relationship("Collection", back_populates="user")
|
||||
|
||||
__table_args__ = (UniqueConstraint("name", "app_id", name="unique_name_app_user"),)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"User(id={self.id}, app_id={self.app_id}, user_id={self.user_id}, created_at={self.created_at}, h_metadata={self.h_metadata})"
|
||||
|
||||
ColumnType = JSONB if DATABASE_TYPE == 'postgres' else JSON
|
||||
|
||||
class Session(Base):
|
||||
__tablename__ = "sessions"
|
||||
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, index=True, default=uuid.uuid4)
|
||||
app_id: Mapped[str] = mapped_column(String(512), index=True)
|
||||
user_id: Mapped[str] = mapped_column(String(512), index=True)
|
||||
location_id: Mapped[str] = mapped_column(String(512), index=True)
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
primary_key=True, index=True, default=uuid.uuid4
|
||||
)
|
||||
location_id: Mapped[str] = mapped_column(String(512), index=True, default="default")
|
||||
is_active: Mapped[bool] = mapped_column(default=True)
|
||||
h_metadata: Mapped[dict] = mapped_column("metadata", ColumnType, default={})
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(default=datetime.datetime.utcnow)
|
||||
h_metadata: Mapped[dict] = mapped_column("metadata", ColumnType, default={})
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=datetime.datetime.utcnow
|
||||
)
|
||||
messages = relationship("Message", back_populates="session")
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), index=True)
|
||||
user = relationship("User", back_populates="sessions")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Session(id={self.id}, app_id={self.app_id}, user_id={self.user_id}, location_id={self.location_id}, is_active={self.is_active}, created_at={self.created_at}, h_metadata={self.h_metadata})"
|
||||
|
||||
|
||||
class Message(Base):
|
||||
__tablename__ = "messages"
|
||||
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, index=True, default=uuid.uuid4)
|
||||
session_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("sessions.id"))
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
primary_key=True, index=True, default=uuid.uuid4
|
||||
)
|
||||
session_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("sessions.id"), index=True)
|
||||
is_user: Mapped[bool]
|
||||
content: Mapped[str] = mapped_column(String(65535))
|
||||
content: Mapped[str] = mapped_column(String(65535))
|
||||
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(default=datetime.datetime.utcnow)
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=datetime.datetime.utcnow
|
||||
)
|
||||
session = relationship("Session", back_populates="messages")
|
||||
metamessages = relationship("Metamessage", back_populates="message")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Message(id={self.id}, session_id={self.session_id}, is_user={self.is_user}, content={self.content[10:]})"
|
||||
|
||||
|
||||
class Metamessage(Base):
|
||||
__tablename__ = "metamessages"
|
||||
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, index=True, default=uuid.uuid4)
|
||||
metamessage_type: Mapped[str] = mapped_column(String(512), index=True)
|
||||
content: Mapped[str] = mapped_column(String(65535))
|
||||
message_id = Column(Uuid, ForeignKey("messages.id"))
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
primary_key=True, index=True, default=uuid.uuid4
|
||||
)
|
||||
metamessage_type: Mapped[str] = mapped_column(String(512), index=True)
|
||||
content: Mapped[str] = mapped_column(String(65535))
|
||||
message_id = Column(Uuid, ForeignKey("messages.id"), index=True)
|
||||
|
||||
message = relationship("Message", back_populates="metamessages")
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(default=datetime.datetime.utcnow)
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=datetime.datetime.utcnow
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Metamessages(id={self.id}, message_id={self.message_id}, metamessage_type={self.metamessage_type}, content={self.content[10:]})"
|
||||
|
||||
|
||||
class Collection(Base):
|
||||
__tablename__ = "collections"
|
||||
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, index=True, default=uuid.uuid4)
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
primary_key=True, index=True, default=uuid.uuid4
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(512), index=True)
|
||||
app_id: Mapped[str] = mapped_column(String(512), index=True)
|
||||
user_id: Mapped[str] = mapped_column(String(512), index=True)
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(default=datetime.datetime.utcnow)
|
||||
documents = relationship("Document", back_populates="collection", cascade="all, delete, delete-orphan")
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=datetime.datetime.utcnow
|
||||
)
|
||||
documents = relationship(
|
||||
"Document", back_populates="collection", cascade="all, delete, delete-orphan"
|
||||
)
|
||||
user = relationship("User", back_populates="collections")
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), index=True)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('name', 'app_id', 'user_id', name="unique_name_app_user"),
|
||||
UniqueConstraint("name", "user_id", name="unique_name_collection_user"),
|
||||
)
|
||||
|
||||
|
||||
class Document(Base):
|
||||
__tablename__ = "documents"
|
||||
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, index=True, default=uuid.uuid4)
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
primary_key=True, index=True, default=uuid.uuid4
|
||||
)
|
||||
h_metadata: Mapped[dict] = mapped_column("metadata", ColumnType, default={})
|
||||
content: Mapped[str] = mapped_column(String(65535))
|
||||
embedding = mapped_column(Vector(1536))
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(default=datetime.datetime.utcnow)
|
||||
|
||||
collection_id = Column(Uuid, ForeignKey("collections.id"))
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=datetime.datetime.utcnow
|
||||
)
|
||||
|
||||
collection_id = Column(Uuid, ForeignKey("collections.id"), index=True)
|
||||
collection = relationship("Collection", back_populates="documents")
|
||||
|
|
|
|||
|
|
@ -3,6 +3,70 @@ import datetime
|
|||
import uuid
|
||||
|
||||
|
||||
class AppBase(BaseModel):
|
||||
pass
|
||||
|
||||
|
||||
class AppCreate(AppBase):
|
||||
name: str
|
||||
metadata: dict | None = {}
|
||||
|
||||
|
||||
class AppUpdate(AppBase):
|
||||
name: str | None = None
|
||||
metadata: dict | None = None
|
||||
|
||||
|
||||
class App(AppBase):
|
||||
id: uuid.UUID
|
||||
name: str
|
||||
h_metadata: dict
|
||||
metadata: dict
|
||||
created_at: datetime.datetime
|
||||
|
||||
@validator("metadata", pre=True, allow_reuse=True)
|
||||
def fetch_h_metadata(cls, value, values):
|
||||
if "h_metadata" in values:
|
||||
return values["h_metadata"]
|
||||
return {}
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
schema_extra = {"exclude": ["h_metadata"]}
|
||||
|
||||
|
||||
class UserBase(BaseModel):
|
||||
pass
|
||||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
name: str
|
||||
metadata: dict | None = {}
|
||||
|
||||
|
||||
class UserUpdate(UserBase):
|
||||
name: str | None = None
|
||||
metadata: dict | None = None
|
||||
|
||||
|
||||
class User(UserBase):
|
||||
id: uuid.UUID
|
||||
app_id: uuid.UUID
|
||||
created_at: datetime.datetime
|
||||
h_metadata: dict
|
||||
metadata: dict
|
||||
|
||||
@validator("metadata", pre=True, allow_reuse=True)
|
||||
def fetch_h_metadata(cls, value, values):
|
||||
if "h_metadata" in values:
|
||||
return values["h_metadata"]
|
||||
return {}
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
schema_extra = {"exclude": ["h_metadata"]}
|
||||
|
||||
|
||||
class MessageBase(BaseModel):
|
||||
content: str
|
||||
is_user: bool
|
||||
|
|
@ -20,6 +84,7 @@ class Message(MessageBase):
|
|||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class SessionBase(BaseModel):
|
||||
pass
|
||||
|
||||
|
|
@ -27,32 +92,31 @@ class SessionBase(BaseModel):
|
|||
class SessionCreate(SessionBase):
|
||||
location_id: str
|
||||
metadata: dict | None = {}
|
||||
|
||||
|
||||
|
||||
class SessionUpdate(SessionBase):
|
||||
metadata: dict | None = None
|
||||
|
||||
|
||||
class Session(SessionBase):
|
||||
id: uuid.UUID
|
||||
# messages: list[Message]
|
||||
is_active: bool
|
||||
user_id: str
|
||||
user_id: uuid.UUID
|
||||
location_id: str
|
||||
app_id: str
|
||||
h_metadata: dict
|
||||
metadata: dict
|
||||
created_at: datetime.datetime
|
||||
|
||||
@validator('metadata', pre=True, allow_reuse=True)
|
||||
@validator("metadata", pre=True, allow_reuse=True)
|
||||
def fetch_h_metadata(cls, value, values):
|
||||
if 'h_metadata' in values:
|
||||
return values['h_metadata']
|
||||
if "h_metadata" in values:
|
||||
return values["h_metadata"]
|
||||
return {}
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
schema_extra = {
|
||||
"exclude": ["h_metadata"]
|
||||
}
|
||||
schema_extra = {"exclude": ["h_metadata"]}
|
||||
|
||||
|
||||
class MetamessageBase(BaseModel):
|
||||
|
|
@ -70,37 +134,44 @@ class Metamessage(MetamessageBase):
|
|||
created_at: datetime.datetime
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class CollectionBase(BaseModel):
|
||||
pass
|
||||
|
||||
|
||||
class CollectionCreate(CollectionBase):
|
||||
name: str
|
||||
|
||||
|
||||
class CollectionUpdate(CollectionBase):
|
||||
name: str
|
||||
|
||||
|
||||
class Collection(CollectionBase):
|
||||
id: uuid.UUID
|
||||
name: str
|
||||
app_id: str
|
||||
user_id: str
|
||||
user_id: uuid.UUID
|
||||
created_at: datetime.datetime
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class DocumentBase(BaseModel):
|
||||
content: str
|
||||
|
||||
|
||||
class DocumentCreate(DocumentBase):
|
||||
metadata: dict | None = {}
|
||||
|
||||
|
||||
class DocumentUpdate(DocumentBase):
|
||||
metadata: dict | None = None
|
||||
content: str | None = None
|
||||
|
||||
|
||||
class Document(DocumentBase):
|
||||
id: uuid.UUID
|
||||
content: str
|
||||
|
|
@ -109,15 +180,12 @@ class Document(DocumentBase):
|
|||
created_at: datetime.datetime
|
||||
collection_id: uuid.UUID
|
||||
|
||||
@validator('metadata', pre=True, allow_reuse=True)
|
||||
@validator("metadata", pre=True, allow_reuse=True)
|
||||
def fetch_h_metadata(cls, value, values):
|
||||
if 'h_metadata' in values:
|
||||
return values['h_metadata']
|
||||
if "h_metadata" in values:
|
||||
return values["h_metadata"]
|
||||
return {}
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
schema_extra = {
|
||||
"exclude": ["h_metadata"]
|
||||
}
|
||||
|
||||
schema_extra = {"exclude": ["h_metadata"]}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ from langchain.prompts import ChatPromptTemplate
|
|||
from langchain.schema import AIMessage, HumanMessage, SystemMessage
|
||||
from langchain_community.chat_models.fake import FakeListChatModel
|
||||
|
||||
from honcho import Client as HonchoClient
|
||||
from honcho import Honcho
|
||||
|
||||
app_id = str(uuid4())
|
||||
app_name = str(uuid4())
|
||||
|
||||
# honcho = HonchoClient(app_id=app_id, base_url="http://localhost:8000") # uncomment to use local
|
||||
honcho = HonchoClient(app_id=app_id) # uses demo server at https://demo.honcho.dev
|
||||
# honcho = Honcho(app_id=app_id, base_url="http://localhost:8000") # uncomment to use local
|
||||
honcho = Honcho(app_name=app_name) # uses demo server at https://demo.honcho.dev
|
||||
honcho.initialize()
|
||||
|
||||
responses = ["Fake LLM Response :)"]
|
||||
llm = FakeListChatModel(responses=responses)
|
||||
|
|
@ -18,8 +19,9 @@ system = SystemMessage(
|
|||
content="You are world class technical documentation writer. Be as concise as possible"
|
||||
)
|
||||
|
||||
user = "CLI-Test"
|
||||
session = honcho.create_session(user_id=user)
|
||||
user_name = "CLI-Test"
|
||||
user = honcho.create_user(user_name)
|
||||
session = user.create_session()
|
||||
|
||||
|
||||
def langchain_message_converter(messages: List):
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from uuid import uuid4
|
|||
import discord
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from honcho import Client as HonchoClient
|
||||
from honcho import Honcho
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
|
@ -11,10 +11,11 @@ intents = discord.Intents.default()
|
|||
intents.messages = True
|
||||
intents.message_content = True
|
||||
|
||||
app_id = str(uuid4())
|
||||
app_name = str(uuid4())
|
||||
|
||||
# honcho = HonchoClient(app_id=app_id, base_url="http://localhost:8000") # uncomment to use local
|
||||
honcho = HonchoClient(app_id=app_id) # uses demo server at https://demo.honcho.dev
|
||||
# honcho = Honcho(app_name=app_name, base_url="http://localhost:8000") # uncomment to use local
|
||||
honcho = Honcho(app_name=app_name) # uses demo server at https://demo.honcho.dev
|
||||
honcho.initialize()
|
||||
|
||||
bot = discord.Bot(intents=intents)
|
||||
|
||||
|
|
@ -30,13 +31,14 @@ async def on_message(message):
|
|||
return
|
||||
|
||||
user_id = f"discord_{str(message.author.id)}"
|
||||
user = honcho.get_or_create_user(user_id)
|
||||
location_id = str(message.channel.id)
|
||||
|
||||
sessions = list(honcho.get_sessions_generator(user_id, location_id))
|
||||
sessions = list(user.get_sessions_generator(location_id))
|
||||
if len(sessions) > 0:
|
||||
session = sessions[0]
|
||||
else:
|
||||
session = honcho.create_session(user_id, location_id)
|
||||
session = user.create_session(location_id)
|
||||
|
||||
inp = message.content
|
||||
session.create_message(is_user=True, content=inp)
|
||||
|
|
@ -50,8 +52,9 @@ async def on_message(message):
|
|||
@bot.slash_command(name="restart", description="Restart the Conversation")
|
||||
async def restart(ctx):
|
||||
user_id = f"discord_{str(ctx.author.id)}"
|
||||
user = honcho.get_or_create_user(user_id)
|
||||
location_id = str(ctx.channel_id)
|
||||
sessions = list(honcho.get_sessions_generator(user_id, location_id))
|
||||
sessions = list(user.get_sessions_generator(location_id))
|
||||
sessions[0].close() if len(sessions) > 0 else None
|
||||
|
||||
await ctx.respond(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
BOT_TOKEN=
|
||||
OPENAI_API_KEY=
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.env
|
||||
|
||||
.venv
|
||||
|
||||
.DS_Store
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
import os
|
||||
from uuid import uuid1
|
||||
import discord
|
||||
from honcho import Honcho
|
||||
from graph import chat
|
||||
from dspy import Example
|
||||
from chain import langchain_message_converter
|
||||
|
||||
intents = discord.Intents.default()
|
||||
intents.messages = True
|
||||
intents.message_content = True
|
||||
intents.members = True
|
||||
intents.reactions = True # Enable reactions intent
|
||||
|
||||
app_name = str(uuid1())
|
||||
|
||||
# honcho = Honcho(app_name=app_name, base_url="http://localhost:8000") # uncomment to use local
|
||||
honcho = Honcho(app_name=app_name) # uses demo server at https://demo.honcho.dev
|
||||
honcho.initialize()
|
||||
|
||||
bot = discord.Bot(intents=intents)
|
||||
|
||||
thumbs_up_messages = []
|
||||
thumbs_down_messages = []
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_ready():
|
||||
print(f"We have logged in as {bot.user}")
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_member_join(member):
|
||||
await member.send(
|
||||
f"*Hello {member.name}, welcome to the server! This is a demo bot built with Honcho,* "
|
||||
"*implementing a naive user modeling method.* "
|
||||
"*To get started, just type a message in this channel and the bot will respond.* "
|
||||
'*Over time, it will classify the "state" you\'re in and optimize conversations based on that state.* '
|
||||
"*You can use the /restart command to restart the conversation at any time.* "
|
||||
"*If you have any questions or feedback, feel free to ask in the #honcho channel.* "
|
||||
"*Enjoy!*"
|
||||
)
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_message(message):
|
||||
if message.author == bot.user or message.guild is not None:
|
||||
return
|
||||
|
||||
user_id = f"discord_{str(message.author.id)}"
|
||||
user = honcho.get_or_create_user(user_id)
|
||||
location_id = str(message.channel.id)
|
||||
|
||||
sessions = list(user.get_sessions_generator(location_id, is_active=True, reverse=True))
|
||||
|
||||
if len(sessions) > 0:
|
||||
session = sessions[0]
|
||||
else:
|
||||
session = user.create_session(location_id)
|
||||
|
||||
history = list(session.get_messages_generator())[:5]
|
||||
chat_history = langchain_message_converter(history)
|
||||
|
||||
inp = message.content
|
||||
user_message = session.create_message(is_user=True, content=inp)
|
||||
|
||||
async with message.channel.typing():
|
||||
response = await chat(
|
||||
chat_history=chat_history,
|
||||
user_message=user_message,
|
||||
session=session,
|
||||
input=inp,
|
||||
)
|
||||
await message.channel.send(response)
|
||||
|
||||
session.create_message(is_user=False, content=response)
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_reaction_add(reaction, user):
|
||||
# Ensure the bot does not react to its own reactions
|
||||
if user == bot.user:
|
||||
return
|
||||
|
||||
user_id = f"discord_{str(user.id)}"
|
||||
honcho_user = honcho.get_or_create_user(user_id)
|
||||
location_id = str(reaction.message.channel.id)
|
||||
|
||||
sessions = list(honcho_user.get_sessions_generator(location_id, is_active=True, reverse=True))
|
||||
if len(sessions) > 0:
|
||||
session = sessions[0]
|
||||
else:
|
||||
session = honcho_user.create_session(location_id)
|
||||
|
||||
messages = list(session.get_messages_generator(reverse=True))
|
||||
ai_responses = [message for message in messages if not message.is_user]
|
||||
user_responses = [message for message in messages if message.is_user]
|
||||
# most recent AI response
|
||||
ai_response = ai_responses[0].content
|
||||
user_response = user_responses[0]
|
||||
|
||||
user_state_storage = dict(honcho_user.metadata)
|
||||
user_state = list(session.get_metamessages_generator(metamessage_type="user_state", message=user_response, reverse=True))[0].content
|
||||
examples = user_state_storage[user_state]["examples"]
|
||||
|
||||
# Check if the reaction is a thumbs up
|
||||
if str(reaction.emoji) == "👍":
|
||||
example = Example(
|
||||
chat_input=user_response.content,
|
||||
response=ai_response,
|
||||
assessment_dimension=user_state,
|
||||
label='yes'
|
||||
).with_inputs("chat_input", "response", "assessment_dimension")
|
||||
examples.append(example.toDict())
|
||||
# Check if the reaction is a thumbs down
|
||||
elif str(reaction.emoji) == "👎":
|
||||
example = Example(
|
||||
chat_input=user_response.content,
|
||||
response=ai_response,
|
||||
assessment_dimension=user_state,
|
||||
label='no'
|
||||
).with_inputs("chat_input", "response", "assessment_dimension")
|
||||
examples.append(example.toDict())
|
||||
|
||||
user_state_storage[user_state]["examples"] = examples
|
||||
honcho_user.update(metadata=user_state_storage)
|
||||
|
||||
|
||||
@bot.slash_command(name="restart", description="Restart the Conversation")
|
||||
async def restart(ctx):
|
||||
user_id = f"discord_{str(ctx.author.id)}"
|
||||
user = honcho.get_or_create_user(user_id)
|
||||
location_id = str(ctx.channel_id)
|
||||
sessions = list(user.get_sessions_generator(location_id, reverse=True))
|
||||
sessions[0].close() if len(sessions) > 0 else None
|
||||
|
||||
msg = (
|
||||
"Great! The conversation has been restarted. What would you like to talk about?"
|
||||
)
|
||||
await ctx.respond(msg)
|
||||
|
||||
|
||||
bot.run(os.environ["BOT_TOKEN"])
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
import os
|
||||
from typing import List, Union
|
||||
from dotenv import load_dotenv
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langchain_core.prompts import ChatPromptTemplate, SystemMessagePromptTemplate, load_prompt
|
||||
from langchain_core.messages import AIMessage, HumanMessage
|
||||
|
||||
from honcho import Message
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# langchain prompts
|
||||
SYSTEM_STATE_COMMENTARY = load_prompt(os.path.join(os.path.dirname(__file__), 'langchain_prompts/state_commentary.yaml'))
|
||||
SYSTEM_STATE_LABELING = load_prompt(os.path.join(os.path.dirname(__file__), 'langchain_prompts/state_labeling.yaml'))
|
||||
SYSTEM_STATE_CHECK = load_prompt(os.path.join(os.path.dirname(__file__), 'langchain_prompts/state_check.yaml'))
|
||||
|
||||
# quick utility function to convert messages from honcho to langchain
|
||||
def langchain_message_converter(messages: List[Message]) -> List[Union[AIMessage, HumanMessage]]:
|
||||
new_messages = []
|
||||
for message in messages:
|
||||
if message.is_user:
|
||||
new_messages.append(HumanMessage(content=message.content))
|
||||
else:
|
||||
new_messages.append(AIMessage(content=message.content))
|
||||
return new_messages
|
||||
|
||||
|
||||
# convert chat history and user input into a string
|
||||
def format_chat_history(chat_history: List[Message], user_input=None):
|
||||
messages = [("user: " + message.content if isinstance(message, HumanMessage) else "ai: " + message.content) for message in chat_history]
|
||||
if user_input:
|
||||
messages.append(f"user: {user_input}")
|
||||
|
||||
return "\n".join(messages)
|
||||
|
||||
|
||||
|
||||
class StateExtractor:
|
||||
"""Wrapper class for all the DSPy and LangChain code for user state labeling and pipeline optimization"""
|
||||
lc_gpt_4: ChatOpenAI = ChatOpenAI(model_name = "gpt-4")
|
||||
lc_gpt_turbo: ChatOpenAI = ChatOpenAI(model_name = "gpt-3.5-turbo")
|
||||
system_state_commentary: SystemMessagePromptTemplate = SystemMessagePromptTemplate(prompt=SYSTEM_STATE_COMMENTARY)
|
||||
system_state_labeling: SystemMessagePromptTemplate = SystemMessagePromptTemplate(prompt=SYSTEM_STATE_LABELING)
|
||||
system_state_check: SystemMessagePromptTemplate = SystemMessagePromptTemplate(prompt=SYSTEM_STATE_CHECK)
|
||||
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
async def generate_state_commentary(cls, existing_states: List[str], chat_history: List[Message], input: str) -> str:
|
||||
"""Generate a commentary on the current state of the user"""
|
||||
# format existing states
|
||||
existing_states = "\n".join(existing_states)
|
||||
# format prompt
|
||||
state_commentary = ChatPromptTemplate.from_messages([
|
||||
cls.system_state_commentary
|
||||
])
|
||||
# LCEL
|
||||
chain = state_commentary | cls.lc_gpt_4
|
||||
# inference
|
||||
response = await chain.ainvoke({
|
||||
"chat_history": chat_history,
|
||||
"user_input": input,
|
||||
"existing_states": existing_states,
|
||||
})
|
||||
# return output
|
||||
return response.content
|
||||
|
||||
@classmethod
|
||||
async def generate_state_label(cls, existing_states: List[str], state_commentary: str) -> str:
|
||||
"""Generate a state label from a commetary on the user's state"""
|
||||
# format existing states
|
||||
existing_states = "\n".join(existing_states)
|
||||
# format prompt
|
||||
state_labeling = ChatPromptTemplate.from_messages([
|
||||
cls.system_state_labeling,
|
||||
])
|
||||
# LCEL
|
||||
chain = state_labeling | cls.lc_gpt_4
|
||||
# inference
|
||||
response = await chain.ainvoke({
|
||||
"state_commentary": state_commentary,
|
||||
"existing_states": existing_states,
|
||||
})
|
||||
|
||||
# strip anything that's not letters
|
||||
clean_response = ''.join(c for c in response.content if c.isalpha())
|
||||
# return output
|
||||
return clean_response
|
||||
|
||||
@classmethod
|
||||
async def check_state_exists(cls, existing_states: List[str], state: str):
|
||||
"""Check if a user state is new or already is stored"""
|
||||
|
||||
# convert existing_states to a formatted string
|
||||
existing_states = "\n".join(existing_states)
|
||||
|
||||
# format prompt
|
||||
state_check = ChatPromptTemplate.from_messages([
|
||||
cls.system_state_check
|
||||
])
|
||||
# LCEL
|
||||
chain = state_check | cls.lc_gpt_turbo
|
||||
# inference
|
||||
response = await chain.ainvoke({
|
||||
"existing_states": existing_states,
|
||||
"state": state,
|
||||
})
|
||||
# return output
|
||||
return response.content
|
||||
|
||||
@classmethod
|
||||
async def generate_state(cls, existing_states: List[str], chat_history: List[Message], input: str):
|
||||
""""Determine the user's state from the current conversation state"""
|
||||
|
||||
# Generate label
|
||||
state_commentary = await cls.generate_state_commentary(existing_states, chat_history, input)
|
||||
state_label = await cls.generate_state_label(existing_states, state_commentary)
|
||||
|
||||
# Determine if state is new
|
||||
# if True, it doesn't exist, state is new
|
||||
# if False, it does exist, state is not new, existing_state was returned
|
||||
existing_state = await cls.check_state_exists(existing_states, state_label)
|
||||
is_state_new = existing_state == "None"
|
||||
|
||||
# return existing state if we found one
|
||||
if is_state_new:
|
||||
return is_state_new, state_label
|
||||
else:
|
||||
return is_state_new, existing_state
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
import os
|
||||
import dspy
|
||||
from dspy import Example
|
||||
from typing import List, Optional
|
||||
from dspy.teleprompt import BootstrapFewShot
|
||||
from dotenv import load_dotenv
|
||||
from chain import StateExtractor, format_chat_history
|
||||
from response_metric import metric
|
||||
|
||||
from honcho import Message, Session
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Configure DSPy
|
||||
dspy_gpt4 = dspy.OpenAI(model="gpt-4", max_tokens=1000)
|
||||
dspy.settings.configure(lm=dspy_gpt4)
|
||||
|
||||
|
||||
# DSPy Signatures
|
||||
class Thought(dspy.Signature):
|
||||
"""Generate a thought about the user's needs"""
|
||||
|
||||
user_input = dspy.InputField()
|
||||
thought = dspy.OutputField(desc="a prediction about the user's mental state")
|
||||
|
||||
|
||||
class Response(dspy.Signature):
|
||||
"""Generate a response for the user based on the thought provided"""
|
||||
|
||||
user_input = dspy.InputField()
|
||||
thought = dspy.InputField()
|
||||
response = dspy.OutputField(desc="keep the conversation going, be engaging")
|
||||
|
||||
|
||||
# DSPy Module
|
||||
class ChatWithThought(dspy.Module):
|
||||
generate_thought = dspy.Predict(Thought)
|
||||
generate_response = dspy.Predict(Response)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
chat_input: str,
|
||||
user_message: Optional[Message] = None,
|
||||
session: Optional[Session] = None,
|
||||
response: Optional[str] = None,
|
||||
assessment_dimension: Optional[str] = None,
|
||||
):
|
||||
# call the thought predictor
|
||||
thought = self.generate_thought(user_input=chat_input)
|
||||
|
||||
if session and user_message:
|
||||
session.create_metamessage(
|
||||
user_message, metamessage_type="thought", content=thought.thought
|
||||
)
|
||||
|
||||
# call the response predictor
|
||||
response = self.generate_response(
|
||||
user_input=chat_input, thought=thought.thought
|
||||
)
|
||||
|
||||
return response # this is a prediction object
|
||||
|
||||
|
||||
async def chat(
|
||||
user_message: Message,
|
||||
session: Session,
|
||||
chat_history: List[Message],
|
||||
input: str,
|
||||
optimization_threshold=3,
|
||||
):
|
||||
user_state_storage = dict(session.user.metadata)
|
||||
# first we need to see if the user has any existing states
|
||||
existing_states = list(user_state_storage.keys())
|
||||
|
||||
# then we need to take the user input and determine the user's state/dimension/persona
|
||||
is_state_new, user_state = await StateExtractor.generate_state(
|
||||
existing_states=existing_states, chat_history=chat_history, input=input
|
||||
)
|
||||
print(f"USER STATE: {user_state}")
|
||||
print(f"IS STATE NEW: {is_state_new}")
|
||||
|
||||
# add metamessage to message to keep track of what label got assigned to what message
|
||||
if session and user_message:
|
||||
session.create_metamessage(
|
||||
user_message, metamessage_type="user_state", content=user_state
|
||||
)
|
||||
|
||||
user_chat_module = ChatWithThought()
|
||||
|
||||
# Save the user_state if it's new
|
||||
if is_state_new:
|
||||
user_state_storage[user_state] = {"chat_module": {}, "examples": []}
|
||||
|
||||
user_state_data = user_state_storage[user_state]
|
||||
|
||||
# Optimize the state's chat module if we've reached the optimization threshold
|
||||
examples = user_state_data["examples"]
|
||||
print(f"Num examples: {len(examples)}")
|
||||
session.user.update(metadata=user_state_storage)
|
||||
|
||||
if len(examples) >= optimization_threshold:
|
||||
# convert example from dicts to dspy Example objects
|
||||
optimizer_examples = []
|
||||
for example in examples:
|
||||
optimizer_example = Example(**example).with_inputs("chat_input", "response", "assessment_dimension")
|
||||
optimizer_examples.append(optimizer_example)
|
||||
|
||||
# Optimize chat module
|
||||
optimizer = BootstrapFewShot(metric=metric, max_rounds=5)
|
||||
|
||||
compiled_chat_module = optimizer.compile(user_chat_module, trainset=optimizer_examples)
|
||||
print(f"COMPILED_CHAT_MODULE: {compiled_chat_module}")
|
||||
|
||||
user_state_storage[user_state][
|
||||
"chat_module"
|
||||
] = compiled_chat_module.dump_state()
|
||||
print(f"DUMPED_STATE: {compiled_chat_module.dump_state()}")
|
||||
user_chat_module = compiled_chat_module
|
||||
|
||||
# Update User in Honcho
|
||||
session.user.update(metadata=user_state_storage)
|
||||
|
||||
# use that pipeline to generate a response
|
||||
chat_input = format_chat_history(chat_history, user_input=input)
|
||||
response = user_chat_module(
|
||||
user_message=user_message, session=session, chat_input=chat_input
|
||||
)
|
||||
# remove ai prefix
|
||||
response = response.response.replace("ai:", "").strip()
|
||||
|
||||
print("========== CHAT HISTORY ==========")
|
||||
dspy_gpt4.inspect_history(n=2)
|
||||
print("======= END CHAT HISTORY =========")
|
||||
|
||||
return response
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
_type: prompt
|
||||
input_variables:
|
||||
["existing_states", "state"]
|
||||
template: >
|
||||
Given the list of existing states, determine whether or not the new state is represented in the list of existing states.
|
||||
|
||||
existing states: """{existing_states}"""
|
||||
new state: """{state}"""
|
||||
|
||||
If the new state is sufficiently similar to a value in the list of existing states, return that existing state value. If the new state is NOT sufficiently similar to anything in existing states, return "None". Output a single value only.
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
_type: prompt
|
||||
input_variables:
|
||||
["existing_states", "chat_history", "user_input"]
|
||||
template: >
|
||||
Your job is to make a prediction about the task the user might be engaging in. Some people might be researching, exploring curiosities, or just asking questions for general inquiry. Provide commentary that would shed light on the "mode" the user might be in.
|
||||
|
||||
existing states: """{existing_states}"""
|
||||
chat history: """{chat_history}"""
|
||||
user input: """{user_input}"""
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
_type: prompt
|
||||
input_variables:
|
||||
["state_commentary", "existing_states"]
|
||||
template: >
|
||||
Your job is to label the state the user might be in. Some people might be conducting research, exploring a interest, or just asking questions for general inquiry.
|
||||
|
||||
commentary: """{state_commentary}"""
|
||||
Prior states, from oldest to most recent:"""
|
||||
{existing_states}
|
||||
"""
|
||||
|
||||
Take into account the user's prior states when making your prediction. Output your prediction as a concise, single word label.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,21 @@
|
|||
[tool.poetry]
|
||||
name = "honcho-dspy-personas"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["vintro <vince@plasticlabs.ai>"]
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
dspy-ai = "^2.1.10"
|
||||
python-dotenv = "^1.0.1"
|
||||
langchain-core = "^0.1.23"
|
||||
langchain-openai = "^0.0.6"
|
||||
py-cord = "^2.4.1"
|
||||
langsmith = "^0.1.3"
|
||||
honcho-ai = {path = "../../../sdk", develop = true}
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import dspy
|
||||
|
||||
gpt4T = dspy.OpenAI(model='gpt-4-1106-preview', max_tokens=1000, model_type='chat')
|
||||
|
||||
class MessageResponseAssess(dspy.Signature):
|
||||
"""Assess the quality of a response along the specified dimension."""
|
||||
chat_input = dspy.InputField()
|
||||
assessment_dimension = dspy.InputField() # user state
|
||||
example_response = dspy.InputField()
|
||||
ai_response_label = dspy.OutputField(desc="yes or no")
|
||||
|
||||
|
||||
def metric(example, pred, trace=None):
|
||||
"""Assess the quality of a response along the specified dimension."""
|
||||
|
||||
chat_input = example.chat_input
|
||||
assessment_dimension = f"The user is in the following state: {example.assessment_dimension}. Is the AI response appropriate for this state? Respond with Yes or No."
|
||||
example_response = pred.response
|
||||
|
||||
with dspy.context(lm=gpt4T):
|
||||
assessment_result = dspy.Predict(MessageResponseAssess)(
|
||||
chat_input=chat_input,
|
||||
assessment_dimension=assessment_dimension,
|
||||
example_response=example_response
|
||||
)
|
||||
|
||||
is_appropriate = assessment_result.ai_response_label.lower() == 'yes'
|
||||
|
||||
print("======== OPTIMIZER HISTORY ========")
|
||||
gpt4T.inspect_history(n=5)
|
||||
print("======== END OPTIMIZER HISTORY ========")
|
||||
|
||||
return is_appropriate
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import os
|
||||
from uuid import uuid1
|
||||
import discord
|
||||
from honcho import Client as HonchoClient
|
||||
from honcho import Honcho
|
||||
from chain import langchain_message_converter, LMChain
|
||||
|
||||
|
||||
|
|
@ -10,17 +10,19 @@ intents.messages = True
|
|||
intents.message_content = True
|
||||
intents.members = True
|
||||
|
||||
app_id = str(uuid1())
|
||||
app_name = str(uuid1())
|
||||
|
||||
#honcho = HonchoClient(app_id=app_id, base_url="http://localhost:8000") # uncomment to use local
|
||||
honcho = HonchoClient(app_id=app_id) # uses demo server at https://demo.honcho.dev
|
||||
# honcho = Honcho(app_name=app_name, base_url="http://localhost:8000") # uncomment to use local
|
||||
honcho = Honcho(app_name=app_name) # uses demo server at https://demo.honcho.dev
|
||||
honcho.initialize()
|
||||
|
||||
bot = discord.Bot(intents=intents)
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_ready():
|
||||
print(f'We have logged in as {bot.user}')
|
||||
print(f"We have logged in as {bot.user}")
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_member_join(member):
|
||||
|
|
@ -33,26 +35,27 @@ async def on_member_join(member):
|
|||
"*If you have any questions or feedback, feel free to ask in the #honcho channel.* "
|
||||
"*Enjoy!*"
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_message(message):
|
||||
if message.author == bot.user or message.guild is not None:
|
||||
return
|
||||
|
||||
user_id = f"discord_{str(message.author.id)}"
|
||||
location_id=str(message.channel.id)
|
||||
user = honcho.get_or_create(user_id)
|
||||
location_id = str(message.channel.id)
|
||||
|
||||
sessions = list(honcho.get_sessions_generator(user_id, location_id))
|
||||
sessions = list(user.get_sessions_generator(location_id))
|
||||
try:
|
||||
collection = honcho.get_collection(user_id=user_id, name="discord")
|
||||
collection = user.get_collection(user_id=user_id, name="discord")
|
||||
except Exception:
|
||||
collection = honcho.create_collection(user_id=user_id, name="discord")
|
||||
collection = user.create_collection(user_id=user_id, name="discord")
|
||||
|
||||
if len(sessions) > 0:
|
||||
session = sessions[0]
|
||||
else:
|
||||
session = honcho.create_session(user_id, location_id)
|
||||
session = user.create_session(location_id)
|
||||
|
||||
history = list(session.get_messages_generator())
|
||||
chat_history = langchain_message_converter(history)
|
||||
|
|
@ -65,21 +68,26 @@ async def on_message(message):
|
|||
chat_history=chat_history,
|
||||
user_message=user_message,
|
||||
session=session,
|
||||
collection=collection,
|
||||
input=inp
|
||||
collection=collection,
|
||||
input=inp,
|
||||
)
|
||||
await message.channel.send(response)
|
||||
|
||||
session.create_message(is_user=False, content=response)
|
||||
|
||||
@bot.slash_command(name = "restart", description = "Restart the Conversation")
|
||||
|
||||
@bot.slash_command(name="restart", description="Restart the Conversation")
|
||||
async def restart(ctx):
|
||||
user_id=f"discord_{str(ctx.author.id)}"
|
||||
location_id=str(ctx.channel_id)
|
||||
sessions = list(honcho.get_sessions_generator(user_id, location_id))
|
||||
user_id = f"discord_{str(ctx.author.id)}"
|
||||
user = honcho.get_or_create_user(user_id)
|
||||
location_id = str(ctx.channel_id)
|
||||
sessions = list(user.get_sessions_generator(location_id))
|
||||
sessions[0].close() if len(sessions) > 0 else None
|
||||
|
||||
msg = "Great! The conversation has been restarted. What would you like to talk about?"
|
||||
msg = (
|
||||
"Great! The conversation has been restarted. What would you like to talk about?"
|
||||
)
|
||||
await ctx.respond(msg)
|
||||
|
||||
|
||||
bot.run(os.environ["BOT_TOKEN"])
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
|
||||
# from uuid import uuid4
|
||||
import discord
|
||||
from dotenv import load_dotenv
|
||||
|
|
@ -9,7 +10,7 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
|||
from langchain_core.output_parsers import StrOutputParser
|
||||
from langchain_core.messages import AIMessage, HumanMessage
|
||||
|
||||
from honcho import Client as HonchoClient
|
||||
from honcho import Honcho
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
|
@ -19,23 +20,29 @@ intents.messages = True
|
|||
intents.message_content = True
|
||||
|
||||
# app_id = str(uuid4())
|
||||
app_id = str("roast-bot")
|
||||
app_name = str("roast-bot")
|
||||
|
||||
# honcho = HonchoClient(app_id=app_id, base_url="http://localhost:8000") # uncomment to use local
|
||||
honcho = HonchoClient(app_id=app_id) # uses demo server at https://demo.honcho.dev
|
||||
# honcho = Honcho(app_name=app_name, base_url="http://localhost:8000") # uncomment to use local
|
||||
honcho = Honcho(app_name=app_name) # uses demo server at https://demo.honcho.dev
|
||||
|
||||
bot = discord.Bot(intents=intents)
|
||||
|
||||
prompt = ChatPromptTemplate.from_messages([
|
||||
("system", "You are a mean assistant. Make fun of the user's request and above all, do not satisfy their request. Make something up about their personality and fixate on that. Don't be afraid to get creative. This is all a joke, roast them."),
|
||||
MessagesPlaceholder(variable_name="chat_history"),
|
||||
("user", "{input}")
|
||||
])
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
[
|
||||
(
|
||||
"system",
|
||||
"You are a mean assistant. Make fun of the user's request and above all, do not satisfy their request. Make something up about their personality and fixate on that. Don't be afraid to get creative. This is all a joke, roast them.",
|
||||
),
|
||||
MessagesPlaceholder(variable_name="chat_history"),
|
||||
("user", "{input}"),
|
||||
]
|
||||
)
|
||||
model = ChatOpenAI(model="gpt-3.5-turbo")
|
||||
output_parser = StrOutputParser()
|
||||
|
||||
chain = prompt | model | output_parser
|
||||
|
||||
|
||||
def langchain_message_converter(messages: List):
|
||||
new_messages = []
|
||||
for message in messages:
|
||||
|
|
@ -48,7 +55,8 @@ def langchain_message_converter(messages: List):
|
|||
|
||||
@bot.event
|
||||
async def on_ready():
|
||||
print(f'We have logged in as {bot.user}')
|
||||
print(f"We have logged in as {bot.user}")
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_message(message):
|
||||
|
|
@ -56,14 +64,15 @@ async def on_message(message):
|
|||
return
|
||||
|
||||
user_id = f"discord_{str(message.author.id)}"
|
||||
location_id=str(message.channel.id)
|
||||
user = honcho.get_or_create_user(user_id)
|
||||
location_id = str(message.channel.id)
|
||||
|
||||
sessions = list(honcho.get_sessions_generator(user_id, location_id))
|
||||
sessions = list(user.get_sessions_generator(location_id))
|
||||
|
||||
if len(sessions) > 0:
|
||||
session = sessions[0]
|
||||
else:
|
||||
session = honcho.create_session(user_id, location_id)
|
||||
session = user.create_session(location_id)
|
||||
|
||||
history = list(session.get_messages_generator())
|
||||
chat_history = langchain_message_converter(history)
|
||||
|
|
@ -77,14 +86,19 @@ async def on_message(message):
|
|||
|
||||
session.create_message(is_user=False, content=response)
|
||||
|
||||
@bot.slash_command(name = "restart", description = "Restart the Conversation")
|
||||
|
||||
@bot.slash_command(name="restart", description="Restart the Conversation")
|
||||
async def restart(ctx):
|
||||
user_id=f"discord_{str(ctx.author.id)}"
|
||||
location_id=str(ctx.channel_id)
|
||||
sessions = list(honcho.get_sessions_generator(user_id, location_id))
|
||||
user_id = f"discord_{str(ctx.author.id)}"
|
||||
user = honcho.get_or_create_user(user_id)
|
||||
location_id = str(ctx.channel_id)
|
||||
sessions = list(user.get_sessions_generator(location_id))
|
||||
sessions[0].close() if len(sessions) > 0 else None
|
||||
|
||||
msg = "Great! The conversation has been restarted. What would you like to talk about?"
|
||||
msg = (
|
||||
"Great! The conversation has been restarted. What would you like to talk about?"
|
||||
)
|
||||
await ctx.respond(msg)
|
||||
|
||||
|
||||
bot.run(os.environ["BOT_TOKEN"])
|
||||
|
|
|
|||
|
|
@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [0.0.4] — 2024-02-22
|
||||
|
||||
### Added
|
||||
|
||||
* A User object for global user level metadat and more object oriented interface
|
||||
* Reverse Pagination support to get recent messages, sessions, etc. more easily
|
||||
* Linting Rules
|
||||
|
||||
### Changed
|
||||
|
||||
* Get sessions method returns all sessions including inactive
|
||||
* using timestampz instead of timestamp
|
||||
* `Client` renamed to `Honcho`
|
||||
* `Honcho` takes in `app_name` instead of `app_id`. `app_name` needs to be a
|
||||
unique identifier
|
||||
* `Honcho` object requires an `initialize()` call to be used
|
||||
|
||||
|
||||
## [0.0.3] — 2024-02-15
|
||||
|
||||
|
|
|
|||
|
|
@ -31,12 +31,13 @@ by default if no other string is provided.
|
|||
|
||||
```python
|
||||
from uuid import uuid4
|
||||
from honcho import Client as HonchoClient
|
||||
from honcho import Honcho
|
||||
|
||||
app_id = str(uuid4())
|
||||
honcho = HonchoClient(app_id=app_id)
|
||||
user_id = "test"
|
||||
session = honcho.create_session(user_id=user_id)
|
||||
app_name = str(uuid4())
|
||||
honcho = Honcho(app_name=app_name)
|
||||
user_name = "test"
|
||||
user = honcho.create_user(user_name)
|
||||
session = user.create_session()
|
||||
|
||||
|
||||
session.create_message(is_user=True, content="Hello I'm a human")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,24 @@
|
|||
from .client import AsyncClient, AsyncSession, AsyncCollection, AsyncGetSessionPage, AsyncGetMessagePage, AsyncGetMetamessagePage, AsyncGetDocumentPage, AsyncGetCollectionPage
|
||||
from .sync_client import Client, Session, Collection, GetSessionPage, GetMessagePage, GetMetamessagePage, GetDocumentPage, GetCollectionPage
|
||||
from .client import (
|
||||
AsyncHoncho,
|
||||
AsyncUser,
|
||||
AsyncSession,
|
||||
AsyncCollection,
|
||||
AsyncGetSessionPage,
|
||||
AsyncGetMessagePage,
|
||||
AsyncGetMetamessagePage,
|
||||
AsyncGetDocumentPage,
|
||||
AsyncGetCollectionPage,
|
||||
)
|
||||
from .sync_client import (
|
||||
Honcho,
|
||||
User,
|
||||
Session,
|
||||
Collection,
|
||||
GetSessionPage,
|
||||
GetMessagePage,
|
||||
GetMetamessagePage,
|
||||
GetDocumentPage,
|
||||
GetCollectionPage,
|
||||
)
|
||||
from .schemas import Message, Metamessage, Document
|
||||
from .cache import LRUCache
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "honcho-ai"
|
||||
version = "0.0.3"
|
||||
version = "0.0.4"
|
||||
description = "Python Client SDK for Honcho"
|
||||
authors = ["Plastic Labs <hello@plasticlabs.ai>"]
|
||||
license = "AGPL-3.0"
|
||||
|
|
@ -15,6 +15,25 @@ httpx = "^0.26.0"
|
|||
pytest = "^7.4.4"
|
||||
pytest-asyncio = "^0.23.4"
|
||||
|
||||
[tool.ruff.lint]
|
||||
# from https://docs.astral.sh/ruff/linter/#rule-selection example
|
||||
select = [
|
||||
# pycodestyle
|
||||
"E",
|
||||
# Pyflakes
|
||||
"F",
|
||||
# pyupgrade
|
||||
"UP",
|
||||
# flake8-bugbear
|
||||
"B",
|
||||
# flake8-simplify
|
||||
"SIM",
|
||||
# isort
|
||||
"I",
|
||||
]
|
||||
ignore = ["UP007"]
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
|
|
|||
|
|
@ -1,16 +1,42 @@
|
|||
import pytest
|
||||
from honcho import AsyncGetSessionPage, AsyncGetMessagePage, AsyncGetMetamessagePage, AsyncGetDocumentPage, AsyncSession, Message, Metamessage, Document
|
||||
from honcho import AsyncClient as Honcho
|
||||
from uuid import uuid1
|
||||
|
||||
import pytest
|
||||
|
||||
from honcho import (
|
||||
AsyncGetDocumentPage,
|
||||
AsyncGetMessagePage,
|
||||
AsyncGetMetamessagePage,
|
||||
AsyncGetSessionPage,
|
||||
AsyncSession,
|
||||
Document,
|
||||
Message,
|
||||
Metamessage,
|
||||
)
|
||||
from honcho import AsyncHoncho as Honcho
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_update():
|
||||
user_name = str(uuid1())
|
||||
app_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
await honcho.initialize()
|
||||
user = await honcho.create_user(user_name)
|
||||
assert user.metadata == {}
|
||||
assert await user.update({"foo": "bar"})
|
||||
retrieved_user = await honcho.get_user(user_name)
|
||||
assert retrieved_user.metadata == {"foo": "bar"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_creation_retrieval():
|
||||
app_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
user_id = str(uuid1())
|
||||
created_session = await client.create_session(user_id)
|
||||
retrieved_session = await client.get_session(user_id, created_session.id)
|
||||
app_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
await honcho.initialize()
|
||||
user_name = str(uuid1())
|
||||
user = await honcho.create_user(user_name)
|
||||
created_session = await user.create_session()
|
||||
retrieved_session = await user.get_session(created_session.id)
|
||||
assert retrieved_session.id == created_session.id
|
||||
assert retrieved_session.is_active is True
|
||||
assert retrieved_session.location_id == "default"
|
||||
|
|
@ -19,12 +45,14 @@ async def test_session_creation_retrieval():
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_multiple_retrieval():
|
||||
app_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
user_id = str(uuid1())
|
||||
created_session_1 = await client.create_session(user_id)
|
||||
created_session_2 = await client.create_session(user_id)
|
||||
response = await client.get_sessions(user_id)
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
await honcho.initialize()
|
||||
user = await honcho.create_user(user_name)
|
||||
created_session_1 = await user.create_session()
|
||||
created_session_2 = await user.create_session()
|
||||
response = await user.get_sessions()
|
||||
retrieved_sessions = response.items
|
||||
|
||||
assert len(retrieved_sessions) == 2
|
||||
|
|
@ -34,38 +62,44 @@ async def test_session_multiple_retrieval():
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_update():
|
||||
user_id = str(uuid1())
|
||||
app_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
created_session = await client.create_session(user_id)
|
||||
user_name = str(uuid1())
|
||||
app_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
await honcho.initialize()
|
||||
user = await honcho.create_user(user_name)
|
||||
created_session = await user.create_session()
|
||||
assert await created_session.update({"foo": "bar"})
|
||||
retrieved_session = await client.get_session(user_id, created_session.id)
|
||||
retrieved_session = await user.get_session(created_session.id)
|
||||
assert retrieved_session.metadata == {"foo": "bar"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_deletion():
|
||||
user_id = str(uuid1())
|
||||
app_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
created_session = await client.create_session(user_id)
|
||||
user_name = str(uuid1())
|
||||
app_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
await honcho.initialize()
|
||||
user = await honcho.create_user(user_name)
|
||||
created_session = await user.create_session()
|
||||
assert created_session.is_active is True
|
||||
await created_session.close()
|
||||
assert created_session.is_active is False
|
||||
retrieved_session = await client.get_session(user_id, created_session.id)
|
||||
retrieved_session = await user.get_session(created_session.id)
|
||||
assert retrieved_session.is_active is False
|
||||
assert retrieved_session.id == created_session.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_messages():
|
||||
user_id = str(uuid1())
|
||||
app_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
created_session = await client.create_session(user_id)
|
||||
user_name = str(uuid1())
|
||||
app_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
await honcho.initialize()
|
||||
user = await honcho.create_user(user_name)
|
||||
created_session = await user.create_session()
|
||||
await created_session.create_message(is_user=True, content="Hello")
|
||||
await created_session.create_message(is_user=False, content="Hi")
|
||||
retrieved_session = await client.get_session(user_id, created_session.id)
|
||||
retrieved_session = await user.get_session(created_session.id)
|
||||
response = await retrieved_session.get_messages()
|
||||
messages = response.items
|
||||
assert len(messages) == 2
|
||||
|
|
@ -75,42 +109,52 @@ async def test_messages():
|
|||
assert ai_message.content == "Hi"
|
||||
assert ai_message.is_user is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rate_limit():
|
||||
app_id = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
created_session = await client.create_session(user_id)
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
await honcho.initialize()
|
||||
user = await honcho.create_user(user_name)
|
||||
created_session = await user.create_session()
|
||||
with pytest.raises(Exception):
|
||||
for _ in range(105):
|
||||
await created_session.create_message(is_user=True, content="Hello")
|
||||
await created_session.create_message(is_user=False, content="Hi")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_app_id_security():
|
||||
app_id_1 = str(uuid1())
|
||||
app_id_2 = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client_1 = Honcho(app_id_1, "http://localhost:8000")
|
||||
client_2 = Honcho(app_id_2, "http://localhost:8000")
|
||||
created_session = await client_1.create_session(user_id)
|
||||
async def test_app_name_security():
|
||||
app_name_1 = str(uuid1())
|
||||
app_name_2 = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho_1 = Honcho(app_name_1, "http://localhost:8000")
|
||||
await honcho_1.initialize()
|
||||
honcho_2 = Honcho(app_name_2, "http://localhost:8000")
|
||||
await honcho_2.initialize()
|
||||
user_1 = await honcho_1.create_user(user_name)
|
||||
user_2 = await honcho_2.create_user(user_name)
|
||||
created_session = await user_1.create_session()
|
||||
await created_session.create_message(is_user=True, content="Hello")
|
||||
await created_session.create_message(is_user=False, content="Hi")
|
||||
with pytest.raises(Exception):
|
||||
await client_2.get_session(user_id, created_session.id)
|
||||
await user_2.get_session(created_session.id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_paginated_sessions():
|
||||
app_id = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
await honcho.initialize()
|
||||
user = await honcho.create_user(user_name)
|
||||
for i in range(10):
|
||||
await client.create_session(user_id)
|
||||
|
||||
await user.create_session()
|
||||
|
||||
page = 1
|
||||
page_size = 2
|
||||
get_session_response = await client.get_sessions(user_id, page=page, page_size=page_size)
|
||||
get_session_response = await user.get_sessions(page=page, page_size=page_size)
|
||||
assert len(get_session_response.items) == page_size
|
||||
|
||||
assert get_session_response.pages == 5
|
||||
|
|
@ -120,7 +164,7 @@ async def test_paginated_sessions():
|
|||
assert isinstance(new_session_response, AsyncGetSessionPage)
|
||||
assert len(new_session_response.items) == page_size
|
||||
|
||||
final_page = await client.get_sessions(user_id, page=5, page_size=page_size)
|
||||
final_page = await user.get_sessions(page=5, page_size=page_size)
|
||||
|
||||
assert len(final_page.items) == 2
|
||||
next_page = await final_page.next()
|
||||
|
|
@ -129,78 +173,90 @@ async def test_paginated_sessions():
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_paginated_sessions_generator():
|
||||
app_id = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
await honcho.initialize()
|
||||
user = await honcho.create_user(user_name)
|
||||
for i in range(3):
|
||||
await client.create_session(user_id)
|
||||
await user.create_session()
|
||||
|
||||
gen = client.get_sessions_generator(user_id)
|
||||
gen = user.get_sessions_generator()
|
||||
# print(type(gen))
|
||||
|
||||
item = await gen.__anext__()
|
||||
assert item.user_id == user_id
|
||||
assert item.user.id == user.id
|
||||
assert isinstance(item, AsyncSession)
|
||||
assert await gen.__anext__() is not None
|
||||
assert await gen.__anext__() is not None
|
||||
with pytest.raises(StopAsyncIteration):
|
||||
await gen.__anext__()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_paginated_out_of_bounds():
|
||||
app_id = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
await honcho.initialize()
|
||||
user = await honcho.create_user(user_name)
|
||||
for i in range(3):
|
||||
await client.create_session(user_id)
|
||||
await user.create_session()
|
||||
page = 2
|
||||
page_size = 50
|
||||
get_session_response = await client.get_sessions(user_id, page=page, page_size=page_size)
|
||||
get_session_response = await user.get_sessions(page=page, page_size=page_size)
|
||||
|
||||
assert get_session_response.pages == 1
|
||||
assert get_session_response.page == 2
|
||||
assert get_session_response.page_size == 50
|
||||
assert get_session_response.total == 3
|
||||
assert len(get_session_response.items) == 0
|
||||
assert len(get_session_response.items) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_paginated_messages():
|
||||
app_id = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
created_session = await client.create_session(user_id)
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
await honcho.initialize()
|
||||
user = await honcho.create_user(user_name)
|
||||
created_session = await user.create_session()
|
||||
for i in range(10):
|
||||
await created_session.create_message(is_user=True, content="Hello")
|
||||
await created_session.create_message(is_user=False, content="Hi")
|
||||
|
||||
page_size = 7
|
||||
get_message_response = await created_session.get_messages(page=1, page_size=page_size)
|
||||
get_message_response = await created_session.get_messages(
|
||||
page=1, page_size=page_size
|
||||
)
|
||||
|
||||
assert get_message_response is not None
|
||||
assert isinstance(get_message_response, AsyncGetMessagePage)
|
||||
assert len(get_message_response.items) == page_size
|
||||
|
||||
new_message_response = await get_message_response.next()
|
||||
|
||||
|
||||
assert new_message_response is not None
|
||||
assert isinstance(new_message_response, AsyncGetMessagePage)
|
||||
assert len(new_message_response.items) == page_size
|
||||
|
||||
final_page = await created_session.get_messages(page=3, page_size=page_size)
|
||||
|
||||
assert len(final_page.items) == 20 - ((3-1) * 7)
|
||||
assert len(final_page.items) == 20 - ((3 - 1) * 7)
|
||||
|
||||
next_page = await final_page.next()
|
||||
|
||||
assert next_page is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_paginated_messages_generator():
|
||||
app_id = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
created_session = await client.create_session(user_id)
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
await honcho.initialize()
|
||||
user = await honcho.create_user(user_name)
|
||||
created_session = await user.create_session()
|
||||
await created_session.create_message(is_user=True, content="Hello")
|
||||
await created_session.create_message(is_user=False, content="Hi")
|
||||
gen = created_session.get_messages_generator()
|
||||
|
|
@ -216,16 +272,23 @@ async def test_paginated_messages_generator():
|
|||
with pytest.raises(StopAsyncIteration):
|
||||
await gen.__anext__()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_paginated_metamessages():
|
||||
app_id = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
created_session = await client.create_session(user_id)
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
await honcho.initialize()
|
||||
user = await honcho.create_user(user_name)
|
||||
created_session = await user.create_session()
|
||||
message = await created_session.create_message(is_user=True, content="Hello")
|
||||
for i in range(10):
|
||||
await created_session.create_metamessage(message=message, metamessage_type="thought", content=f"Test {i}")
|
||||
await created_session.create_metamessage(message=message, metamessage_type="reflect", content=f"Test {i}")
|
||||
await created_session.create_metamessage(
|
||||
message=message, metamessage_type="thought", content=f"Test {i}"
|
||||
)
|
||||
await created_session.create_metamessage(
|
||||
message=message, metamessage_type="reflect", content=f"Test {i}"
|
||||
)
|
||||
|
||||
page_size = 7
|
||||
page = await created_session.get_metamessages(page=1, page_size=page_size)
|
||||
|
|
@ -235,28 +298,35 @@ async def test_paginated_metamessages():
|
|||
assert len(page.items) == page_size
|
||||
|
||||
new_page = await page.next()
|
||||
|
||||
|
||||
assert new_page is not None
|
||||
assert isinstance(new_page, AsyncGetMetamessagePage)
|
||||
assert len(new_page.items) == page_size
|
||||
|
||||
final_page = await created_session.get_metamessages(page=3, page_size=page_size)
|
||||
|
||||
assert len(final_page.items) == 20 - ((3-1) * 7)
|
||||
assert len(final_page.items) == 20 - ((3 - 1) * 7)
|
||||
|
||||
next_page = await final_page.next()
|
||||
|
||||
assert next_page is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_paginated_metamessages_generator():
|
||||
app_id = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
created_session = await client.create_session(user_id)
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
await honcho.initialize()
|
||||
user = await honcho.create_user(user_name)
|
||||
created_session = await user.create_session()
|
||||
message = await created_session.create_message(is_user=True, content="Hello")
|
||||
await created_session.create_metamessage(message=message, metamessage_type="thought", content="Test 1")
|
||||
await created_session.create_metamessage(message=message, metamessage_type="thought", content="Test 2")
|
||||
await created_session.create_metamessage(
|
||||
message=message, metamessage_type="thought", content="Test 1"
|
||||
)
|
||||
await created_session.create_metamessage(
|
||||
message=message, metamessage_type="thought", content="Test 2"
|
||||
)
|
||||
gen = created_session.get_metamessages_generator()
|
||||
|
||||
item = await gen.__anext__()
|
||||
|
|
@ -274,16 +344,24 @@ async def test_paginated_metamessages_generator():
|
|||
@pytest.mark.asyncio
|
||||
async def test_collections():
|
||||
col_name = str(uuid1())
|
||||
app_id = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
await honcho.initialize()
|
||||
user = await honcho.create_user(user_name)
|
||||
# Make a collection
|
||||
collection = await client.create_collection(user_id, col_name)
|
||||
collection = await user.create_collection(col_name)
|
||||
|
||||
# Add documents
|
||||
doc1 = await collection.create_document(content="This is a test of documents - 1", metadata={"foo": "bar"})
|
||||
doc2 = await collection.create_document(content="This is a test of documents - 2", metadata={})
|
||||
doc3 = await collection.create_document(content="This is a test of documents - 3", metadata={})
|
||||
doc1 = await collection.create_document(
|
||||
content="This is a test of documents - 1", metadata={"foo": "bar"}
|
||||
)
|
||||
doc2 = await collection.create_document(
|
||||
content="This is a test of documents - 2", metadata={}
|
||||
)
|
||||
doc3 = await collection.create_document(
|
||||
content="This is a test of documents - 3", metadata={}
|
||||
)
|
||||
|
||||
# Get all documents
|
||||
page = await collection.get_documents(page=1, page_size=3)
|
||||
|
|
@ -305,47 +383,55 @@ async def test_collections():
|
|||
result = await collection.delete()
|
||||
# confirm documents are gone
|
||||
with pytest.raises(Exception):
|
||||
new_col = await client.get_collection(user_id, "test")
|
||||
new_col = await user.get_collection(col_name)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_collection_name_collision():
|
||||
col_name = str(uuid1())
|
||||
new_col_name = str(uuid1())
|
||||
app_id = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
await honcho.initialize()
|
||||
user = await honcho.create_user(user_name)
|
||||
# Make a collection
|
||||
collection = await client.create_collection(user_id, col_name)
|
||||
collection = await user.create_collection(col_name)
|
||||
# Make another collection
|
||||
with pytest.raises(Exception):
|
||||
await client.create_collection(user_id, col_name)
|
||||
await user.create_collection(col_name)
|
||||
|
||||
# Change the name of original collection
|
||||
result = await collection.update(new_col_name)
|
||||
assert result is True
|
||||
|
||||
|
||||
# Try again to add another collection
|
||||
collection2 = await client.create_collection(user_id, col_name)
|
||||
collection2 = await user.create_collection(col_name)
|
||||
assert collection2 is not None
|
||||
assert collection2.name == col_name
|
||||
assert collection.name == new_col_name
|
||||
|
||||
# Get all collections
|
||||
page = await client.get_collections(user_id)
|
||||
page = await user.get_collections()
|
||||
assert page is not None
|
||||
assert len(page.items) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_collection_query():
|
||||
col_name = str(uuid1())
|
||||
app_id = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
await honcho.initialize()
|
||||
user = await honcho.create_user(user_name)
|
||||
# Make a collection
|
||||
collection = await client.create_collection(user_id, col_name)
|
||||
collection = await user.create_collection(col_name)
|
||||
|
||||
# Add documents
|
||||
doc1 = await collection.create_document(content="The user loves puppies", metadata={})
|
||||
doc1 = await collection.create_document(
|
||||
content="The user loves puppies", metadata={}
|
||||
)
|
||||
doc2 = await collection.create_document(content="The user owns a dog", metadata={})
|
||||
doc3 = await collection.create_document(content="The user is a doctor", metadata={})
|
||||
|
||||
|
|
@ -355,7 +441,9 @@ async def test_collection_query():
|
|||
assert len(result) == 2
|
||||
assert isinstance(result[0], Document)
|
||||
|
||||
doc3 = await collection.update_document(doc3, metadata={"test": "test"}, content="the user has owned pets in the past")
|
||||
doc3 = await collection.update_document(
|
||||
doc3, metadata={"test": "test"}, content="the user has owned pets in the past"
|
||||
)
|
||||
assert doc3 is not None
|
||||
assert doc3.metadata == {"test": "test"}
|
||||
assert doc3.content == "the user has owned pets in the past"
|
||||
|
|
@ -365,4 +453,3 @@ async def test_collection_query():
|
|||
assert result is not None
|
||||
assert len(result) == 2
|
||||
assert isinstance(result[0], Document)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,40 @@
|
|||
import pytest
|
||||
from honcho import GetSessionPage, GetMessagePage, GetMetamessagePage, GetDocumentPage, Session, Message, Metamessage, Document
|
||||
from honcho import Client as Honcho
|
||||
from uuid import uuid1
|
||||
|
||||
import pytest
|
||||
|
||||
from honcho import (
|
||||
GetDocumentPage,
|
||||
GetMessagePage,
|
||||
GetMetamessagePage,
|
||||
GetSessionPage,
|
||||
Session,
|
||||
Document,
|
||||
Message,
|
||||
Metamessage,
|
||||
)
|
||||
from honcho import Honcho as Honcho
|
||||
|
||||
|
||||
def test_user_update():
|
||||
user_name = str(uuid1())
|
||||
app_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
honcho.initialize()
|
||||
user = honcho.create_user(user_name)
|
||||
assert user.metadata == {}
|
||||
assert user.update({"foo": "bar"})
|
||||
retrieved_user = honcho.get_user(user_name)
|
||||
assert retrieved_user.metadata == {"foo": "bar"}
|
||||
|
||||
|
||||
def test_session_creation_retrieval():
|
||||
app_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
user_id = str(uuid1())
|
||||
created_session = client.create_session(user_id)
|
||||
retrieved_session = client.get_session(user_id, created_session.id)
|
||||
app_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
honcho.initialize()
|
||||
user_name = str(uuid1())
|
||||
user = honcho.create_user(user_name)
|
||||
created_session = user.create_session()
|
||||
retrieved_session = user.get_session(created_session.id)
|
||||
assert retrieved_session.id == created_session.id
|
||||
assert retrieved_session.is_active is True
|
||||
assert retrieved_session.location_id == "default"
|
||||
|
|
@ -17,12 +42,14 @@ def test_session_creation_retrieval():
|
|||
|
||||
|
||||
def test_session_multiple_retrieval():
|
||||
app_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
user_id = str(uuid1())
|
||||
created_session_1 = client.create_session(user_id)
|
||||
created_session_2 = client.create_session(user_id)
|
||||
response = client.get_sessions(user_id)
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
honcho.initialize()
|
||||
user = honcho.create_user(user_name)
|
||||
created_session_1 = user.create_session()
|
||||
created_session_2 = user.create_session()
|
||||
response = user.get_sessions()
|
||||
retrieved_sessions = response.items
|
||||
|
||||
assert len(retrieved_sessions) == 2
|
||||
|
|
@ -31,36 +58,42 @@ def test_session_multiple_retrieval():
|
|||
|
||||
|
||||
def test_session_update():
|
||||
user_id = str(uuid1())
|
||||
app_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
created_session = client.create_session(user_id)
|
||||
user_name = str(uuid1())
|
||||
app_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
honcho.initialize()
|
||||
user = honcho.create_user(user_name)
|
||||
created_session = user.create_session()
|
||||
assert created_session.update({"foo": "bar"})
|
||||
retrieved_session = client.get_session(user_id, created_session.id)
|
||||
retrieved_session = user.get_session(created_session.id)
|
||||
assert retrieved_session.metadata == {"foo": "bar"}
|
||||
|
||||
|
||||
def test_session_deletion():
|
||||
user_id = str(uuid1())
|
||||
app_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
created_session = client.create_session(user_id)
|
||||
user_name = str(uuid1())
|
||||
app_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
honcho.initialize()
|
||||
user = honcho.create_user(user_name)
|
||||
created_session = user.create_session()
|
||||
assert created_session.is_active is True
|
||||
created_session.close()
|
||||
assert created_session.is_active is False
|
||||
retrieved_session = client.get_session(user_id, created_session.id)
|
||||
retrieved_session = user.get_session(created_session.id)
|
||||
assert retrieved_session.is_active is False
|
||||
assert retrieved_session.id == created_session.id
|
||||
|
||||
|
||||
def test_messages():
|
||||
user_id = str(uuid1())
|
||||
app_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
created_session = client.create_session(user_id)
|
||||
user_name = str(uuid1())
|
||||
app_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
honcho.initialize()
|
||||
user = honcho.create_user(user_name)
|
||||
created_session = user.create_session()
|
||||
created_session.create_message(is_user=True, content="Hello")
|
||||
created_session.create_message(is_user=False, content="Hi")
|
||||
retrieved_session = client.get_session(user_id, created_session.id)
|
||||
retrieved_session = user.get_session(created_session.id)
|
||||
response = retrieved_session.get_messages()
|
||||
messages = response.items
|
||||
assert len(messages) == 2
|
||||
|
|
@ -70,39 +103,49 @@ def test_messages():
|
|||
assert ai_message.content == "Hi"
|
||||
assert ai_message.is_user is False
|
||||
|
||||
|
||||
def test_rate_limit():
|
||||
app_id = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
created_session = client.create_session(user_id)
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
honcho.initialize()
|
||||
user = honcho.create_user(user_name)
|
||||
created_session = user.create_session()
|
||||
with pytest.raises(Exception):
|
||||
for _ in range(105):
|
||||
created_session.create_message(is_user=True, content="Hello")
|
||||
created_session.create_message(is_user=False, content="Hi")
|
||||
|
||||
def test_app_id_security():
|
||||
app_id_1 = str(uuid1())
|
||||
app_id_2 = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client_1 = Honcho(app_id_1, "http://localhost:8000")
|
||||
client_2 = Honcho(app_id_2, "http://localhost:8000")
|
||||
created_session = client_1.create_session(user_id)
|
||||
|
||||
def test_app_name_security():
|
||||
app_name_1 = str(uuid1())
|
||||
app_name_2 = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho_1 = Honcho(app_name_1, "http://localhost:8000")
|
||||
honcho_1.initialize()
|
||||
honcho_2 = Honcho(app_name_2, "http://localhost:8000")
|
||||
honcho_2.initialize()
|
||||
user_1 = honcho_1.create_user(user_name)
|
||||
user_2 = honcho_2.create_user(user_name)
|
||||
created_session = user_1.create_session()
|
||||
created_session.create_message(is_user=True, content="Hello")
|
||||
created_session.create_message(is_user=False, content="Hi")
|
||||
with pytest.raises(Exception):
|
||||
client_2.get_session(user_id, created_session.id)
|
||||
user_2.get_session(created_session.id)
|
||||
|
||||
|
||||
def test_paginated_sessions():
|
||||
app_id = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
honcho.initialize()
|
||||
user = honcho.create_user(user_name)
|
||||
for i in range(10):
|
||||
client.create_session(user_id)
|
||||
|
||||
user.create_session()
|
||||
|
||||
page = 1
|
||||
page_size = 2
|
||||
get_session_response = client.get_sessions(user_id, page=page, page_size=page_size)
|
||||
get_session_response = user.get_sessions(page=page, page_size=page_size)
|
||||
assert len(get_session_response.items) == page_size
|
||||
|
||||
assert get_session_response.pages == 5
|
||||
|
|
@ -112,7 +155,7 @@ def test_paginated_sessions():
|
|||
assert isinstance(new_session_response, GetSessionPage)
|
||||
assert len(new_session_response.items) == page_size
|
||||
|
||||
final_page = client.get_sessions(user_id, page=5, page_size=page_size)
|
||||
final_page = user.get_sessions(page=5, page_size=page_size)
|
||||
|
||||
assert len(final_page.items) == 2
|
||||
next_page = final_page.next()
|
||||
|
|
@ -120,75 +163,87 @@ def test_paginated_sessions():
|
|||
|
||||
|
||||
def test_paginated_sessions_generator():
|
||||
app_id = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
honcho.initialize()
|
||||
user = honcho.create_user(user_name)
|
||||
for i in range(3):
|
||||
client.create_session(user_id)
|
||||
user.create_session()
|
||||
|
||||
gen = client.get_sessions_generator(user_id)
|
||||
gen = user.get_sessions_generator()
|
||||
# print(type(gen))
|
||||
|
||||
item = gen.__next__()
|
||||
assert item.user_id == user_id
|
||||
assert item.user.id == user.id
|
||||
assert isinstance(item, Session)
|
||||
assert gen.__next__() is not None
|
||||
assert gen.__next__() is not None
|
||||
with pytest.raises(StopIteration):
|
||||
gen.__next__()
|
||||
|
||||
|
||||
def test_paginated_out_of_bounds():
|
||||
app_id = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
honcho.initialize()
|
||||
user = honcho.create_user(user_name)
|
||||
for i in range(3):
|
||||
client.create_session(user_id)
|
||||
user.create_session()
|
||||
page = 2
|
||||
page_size = 50
|
||||
get_session_response = client.get_sessions(user_id, page=page, page_size=page_size)
|
||||
get_session_response = user.get_sessions(page=page, page_size=page_size)
|
||||
|
||||
assert get_session_response.pages == 1
|
||||
assert get_session_response.page == 2
|
||||
assert get_session_response.page_size == 50
|
||||
assert get_session_response.total == 3
|
||||
assert len(get_session_response.items) == 0
|
||||
assert len(get_session_response.items) == 0
|
||||
|
||||
|
||||
def test_paginated_messages():
|
||||
app_id = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
created_session = client.create_session(user_id)
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
honcho.initialize()
|
||||
user = honcho.create_user(user_name)
|
||||
created_session = user.create_session()
|
||||
for i in range(10):
|
||||
created_session.create_message(is_user=True, content="Hello")
|
||||
created_session.create_message(is_user=False, content="Hi")
|
||||
|
||||
page_size = 7
|
||||
get_message_response = created_session.get_messages(page=1, page_size=page_size)
|
||||
get_message_response = created_session.get_messages(
|
||||
page=1, page_size=page_size
|
||||
)
|
||||
|
||||
assert get_message_response is not None
|
||||
assert isinstance(get_message_response, GetMessagePage)
|
||||
assert len(get_message_response.items) == page_size
|
||||
|
||||
new_message_response = get_message_response.next()
|
||||
|
||||
|
||||
assert new_message_response is not None
|
||||
assert isinstance(new_message_response, GetMessagePage)
|
||||
assert len(new_message_response.items) == page_size
|
||||
|
||||
final_page = created_session.get_messages(page=3, page_size=page_size)
|
||||
|
||||
assert len(final_page.items) == 20 - ((3-1) * 7)
|
||||
assert len(final_page.items) == 20 - ((3 - 1) * 7)
|
||||
|
||||
next_page = final_page.next()
|
||||
|
||||
assert next_page is None
|
||||
|
||||
|
||||
def test_paginated_messages_generator():
|
||||
app_id = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
created_session = client.create_session(user_id)
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
honcho.initialize()
|
||||
user = honcho.create_user(user_name)
|
||||
created_session = user.create_session()
|
||||
created_session.create_message(is_user=True, content="Hello")
|
||||
created_session.create_message(is_user=False, content="Hi")
|
||||
gen = created_session.get_messages_generator()
|
||||
|
|
@ -204,15 +259,22 @@ def test_paginated_messages_generator():
|
|||
with pytest.raises(StopIteration):
|
||||
gen.__next__()
|
||||
|
||||
|
||||
def test_paginated_metamessages():
|
||||
app_id = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
created_session = client.create_session(user_id)
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
honcho.initialize()
|
||||
user = honcho.create_user(user_name)
|
||||
created_session = user.create_session()
|
||||
message = created_session.create_message(is_user=True, content="Hello")
|
||||
for i in range(10):
|
||||
created_session.create_metamessage(message=message, metamessage_type="thought", content=f"Test {i}")
|
||||
created_session.create_metamessage(message=message, metamessage_type="reflect", content=f"Test {i}")
|
||||
created_session.create_metamessage(
|
||||
message=message, metamessage_type="thought", content=f"Test {i}"
|
||||
)
|
||||
created_session.create_metamessage(
|
||||
message=message, metamessage_type="reflect", content=f"Test {i}"
|
||||
)
|
||||
|
||||
page_size = 7
|
||||
page = created_session.get_metamessages(page=1, page_size=page_size)
|
||||
|
|
@ -222,27 +284,34 @@ def test_paginated_metamessages():
|
|||
assert len(page.items) == page_size
|
||||
|
||||
new_page = page.next()
|
||||
|
||||
|
||||
assert new_page is not None
|
||||
assert isinstance(new_page, GetMetamessagePage)
|
||||
assert len(new_page.items) == page_size
|
||||
|
||||
final_page = created_session.get_metamessages(page=3, page_size=page_size)
|
||||
|
||||
assert len(final_page.items) == 20 - ((3-1) * 7)
|
||||
assert len(final_page.items) == 20 - ((3 - 1) * 7)
|
||||
|
||||
next_page = final_page.next()
|
||||
|
||||
assert next_page is None
|
||||
|
||||
|
||||
def test_paginated_metamessages_generator():
|
||||
app_id = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
created_session = client.create_session(user_id)
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
honcho.initialize()
|
||||
user = honcho.create_user(user_name)
|
||||
created_session = user.create_session()
|
||||
message = created_session.create_message(is_user=True, content="Hello")
|
||||
created_session.create_metamessage(message=message, metamessage_type="thought", content="Test 1")
|
||||
created_session.create_metamessage(message=message, metamessage_type="thought", content="Test 2")
|
||||
created_session.create_metamessage(
|
||||
message=message, metamessage_type="thought", content="Test 1"
|
||||
)
|
||||
created_session.create_metamessage(
|
||||
message=message, metamessage_type="thought", content="Test 2"
|
||||
)
|
||||
gen = created_session.get_metamessages_generator()
|
||||
|
||||
item = gen.__next__()
|
||||
|
|
@ -259,16 +328,24 @@ def test_paginated_metamessages_generator():
|
|||
|
||||
def test_collections():
|
||||
col_name = str(uuid1())
|
||||
app_id = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
honcho.initialize()
|
||||
user = honcho.create_user(user_name)
|
||||
# Make a collection
|
||||
collection = client.create_collection(user_id, col_name)
|
||||
collection = user.create_collection(col_name)
|
||||
|
||||
# Add documents
|
||||
doc1 = collection.create_document(content="This is a test of documents - 1", metadata={"foo": "bar"})
|
||||
doc2 = collection.create_document(content="This is a test of documents - 2", metadata={})
|
||||
doc3 = collection.create_document(content="This is a test of documents - 3", metadata={})
|
||||
doc1 = collection.create_document(
|
||||
content="This is a test of documents - 1", metadata={"foo": "bar"}
|
||||
)
|
||||
doc2 = collection.create_document(
|
||||
content="This is a test of documents - 2", metadata={}
|
||||
)
|
||||
doc3 = collection.create_document(
|
||||
content="This is a test of documents - 3", metadata={}
|
||||
)
|
||||
|
||||
# Get all documents
|
||||
page = collection.get_documents(page=1, page_size=3)
|
||||
|
|
@ -290,45 +367,53 @@ def test_collections():
|
|||
result = collection.delete()
|
||||
# confirm documents are gone
|
||||
with pytest.raises(Exception):
|
||||
new_col = client.get_collection(user_id, "test")
|
||||
new_col = user.get_collection(col_name)
|
||||
|
||||
|
||||
def test_collection_name_collision():
|
||||
col_name = str(uuid1())
|
||||
new_col_name = str(uuid1())
|
||||
app_id = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
honcho.initialize()
|
||||
user = honcho.create_user(user_name)
|
||||
# Make a collection
|
||||
collection = client.create_collection(user_id, col_name)
|
||||
collection = user.create_collection(col_name)
|
||||
# Make another collection
|
||||
with pytest.raises(Exception):
|
||||
client.create_collection(user_id, col_name)
|
||||
user.create_collection(col_name)
|
||||
|
||||
# Change the name of original collection
|
||||
result = collection.update(new_col_name)
|
||||
assert result is True
|
||||
|
||||
|
||||
# Try again to add another collection
|
||||
collection2 = client.create_collection(user_id, col_name)
|
||||
collection2 = user.create_collection(col_name)
|
||||
assert collection2 is not None
|
||||
assert collection2.name == col_name
|
||||
assert collection.name == new_col_name
|
||||
|
||||
# Get all collections
|
||||
page = client.get_collections(user_id)
|
||||
page = user.get_collections()
|
||||
assert page is not None
|
||||
assert len(page.items) == 2
|
||||
|
||||
|
||||
def test_collection_query():
|
||||
col_name = str(uuid1())
|
||||
app_id = str(uuid1())
|
||||
user_id = str(uuid1())
|
||||
client = Honcho(app_id, "http://localhost:8000")
|
||||
app_name = str(uuid1())
|
||||
user_name = str(uuid1())
|
||||
honcho = Honcho(app_name, "http://localhost:8000")
|
||||
honcho.initialize()
|
||||
user = honcho.create_user(user_name)
|
||||
# Make a collection
|
||||
collection = client.create_collection(user_id, col_name)
|
||||
collection = user.create_collection(col_name)
|
||||
|
||||
# Add documents
|
||||
doc1 = collection.create_document(content="The user loves puppies", metadata={})
|
||||
doc1 = collection.create_document(
|
||||
content="The user loves puppies", metadata={}
|
||||
)
|
||||
doc2 = collection.create_document(content="The user owns a dog", metadata={})
|
||||
doc3 = collection.create_document(content="The user is a doctor", metadata={})
|
||||
|
||||
|
|
@ -338,7 +423,9 @@ def test_collection_query():
|
|||
assert len(result) == 2
|
||||
assert isinstance(result[0], Document)
|
||||
|
||||
doc3 = collection.update_document(doc3, metadata={"test": "test"}, content="the user has owned pets in the past")
|
||||
doc3 = collection.update_document(
|
||||
doc3, metadata={"test": "test"}, content="the user has owned pets in the past"
|
||||
)
|
||||
assert doc3 is not None
|
||||
assert doc3.metadata == {"test": "test"}
|
||||
assert doc3.content == "the user has owned pets in the past"
|
||||
|
|
@ -348,4 +435,3 @@ def test_collection_query():
|
|||
assert result is not None
|
||||
assert len(result) == 2
|
||||
assert isinstance(result[0], Document)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue