* 🧪 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:
Vineeth Voruganti 2024-02-23 11:22:18 -08:00 committed by GitHub
parent a0a10e5002
commit 5ebb8559bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 5683 additions and 1124 deletions

3
api/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"python.analysis.typeCheckingMode": "basic"
}

View File

@ -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

52
api/poetry.lock generated
View File

@ -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"

View File

@ -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"]

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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")

View File

@ -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"]}

View File

@ -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):

View File

@ -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(

View File

@ -0,0 +1,2 @@
BOT_TOKEN=
OPENAI_API_KEY=

View File

@ -0,0 +1,5 @@
.env
.venv
.DS_Store

View File

@ -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"])

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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}"""

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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"])

View File

@ -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"])

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -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"

View File

@ -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)

View File

@ -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)