* 🧪 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

---------

Co-authored-by: hyusap <paulayush@gmail.com>
Co-authored-by: vintro <vince@plasticlabs.ai>
This commit is contained in:
Vineeth Voruganti 2024-02-08 11:05:56 -08:00 committed by GitHub
parent e7204f101d
commit 8539b87804
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 3581 additions and 518 deletions

2
.gitignore vendored
View File

@ -1,6 +1,6 @@
api/**/*.db
.vscode/
# Byte-compiled / optimized / DLL files
__pycache__/

20
.vscode/honcho.code-workspace vendored Normal file
View File

@ -0,0 +1,20 @@
{
"folders": [
{
"path": "../sdk"
},
{
"path": "../api"
},
{
"path": "../example/cli"
},
{
"path": "../example/discord"
},
{
"path": ".."
}
],
"settings": {}
}

8
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"python.analysis.typeCheckingMode": "basic",
"files.exclude": {
"sdk": true,
"api": true,
"example": true
}
}

View File

@ -1,5 +1,5 @@
# Honcho
![Static Badge](https://img.shields.io/badge/Version-0.0.1-blue)
![Static Badge](https://img.shields.io/badge/Version-0.0.2-blue)
[![Discord](https://img.shields.io/discord/1016845111637839922?style=flat&logo=discord&logoColor=23ffffff&label=Plastic%20Labs&labelColor=235865F2)](https://discord.gg/plasticlabs)
![GitHub License](https://img.shields.io/github/license/plastic-labs/honcho)
![GitHub Repo stars](https://img.shields.io/github/stars/plastic-labs/honcho)

18
api/.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,18 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "start",
"type": "shell",
"command": "poetry run uvicorn src.main:app --reload",
"group": "none",
"presentation": {
"reveal": "always",
"panel": "shared"
},
"runOptions": {
"runOn": "folderOpen"
}
}
]
}

View File

@ -4,6 +4,26 @@ 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.2] — 2024-02-01
### Added
* Pagination for requests via `fastapi_pagination`
* Metamessages
* `get_message` routes
* `created_at` field added to each Table
* Message size limits
### Changed
* IDs are now UUIDs
* default rate limit now 100 requests per minute
### Removed
* Removed messages from session response model
## [0.0.1] — 2024-02-01
### Added

36
api/poetry.lock generated
View File

@ -118,6 +118,40 @@ 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)"]
[[package]]
name = "fastapi-pagination"
version = "0.12.14"
description = "FastAPI pagination"
category = "main"
optional = false
python-versions = ">=3.8,<4.0"
files = [
{file = "fastapi_pagination-0.12.14-py3-none-any.whl", hash = "sha256:59b6c5626b1d21c610da333c7a586d625f6c81d8fa26267a4b598aae736f6753"},
{file = "fastapi_pagination-0.12.14.tar.gz", hash = "sha256:4148694b1e170055eea0a5e691dbc640c4bf55eb0086cf11d14b164c35660559"},
]
[package.dependencies]
fastapi = ">=0.93.0"
pydantic = ">=1.9.1"
typing-extensions = ">=4.8.0,<5.0.0"
[package.extras]
all = ["SQLAlchemy (>=1.3.20)", "asyncpg (>=0.24.0)", "beanie (>=1.11.9,<2.0.0)", "bunnet (>=1.1.0,<2.0.0)", "databases (>=0.6.0)", "django (<5.0.0)", "mongoengine (>=0.23.1,<0.28.0)", "motor (>=2.5.1,<4.0.0)", "orm (>=0.3.1)", "ormar (>=0.11.2)", "piccolo (>=0.89,<0.122)", "pony (>=0.7.16,<0.8.0)", "scylla-driver (>=3.25.6,<4.0.0)", "sqlakeyset (>=2.0.1680321678,<3.0.0)", "sqlmodel (>=0.0.8,<0.0.15)", "tortoise-orm (>=0.16.18,<0.21.0)"]
asyncpg = ["SQLAlchemy (>=1.3.20)", "asyncpg (>=0.24.0)"]
beanie = ["beanie (>=1.11.9,<2.0.0)"]
bunnet = ["bunnet (>=1.1.0,<2.0.0)"]
databases = ["databases (>=0.6.0)"]
django = ["databases (>=0.6.0)", "django (<5.0.0)"]
mongoengine = ["mongoengine (>=0.23.1,<0.28.0)"]
motor = ["motor (>=2.5.1,<4.0.0)"]
orm = ["databases (>=0.6.0)", "orm (>=0.3.1)"]
ormar = ["ormar (>=0.11.2)"]
piccolo = ["piccolo (>=0.89,<0.122)"]
scylla-driver = ["scylla-driver (>=3.25.6,<4.0.0)"]
sqlalchemy = ["SQLAlchemy (>=1.3.20)", "sqlakeyset (>=2.0.1680321678,<3.0.0)"]
sqlmodel = ["sqlakeyset (>=2.0.1680321678,<3.0.0)", "sqlmodel (>=0.0.8,<0.0.15)"]
tortoise = ["tortoise-orm (>=0.16.18,<0.21.0)"]
[[package]]
name = "greenlet"
version = "3.0.3"
@ -778,4 +812,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
content-hash = "ff6b70507443b2f4327c19691815f1f7d2814b16e635ff62b7224f30bd81eeab"
content-hash = "115cde0c7dc1de7906b4f17bbdaced3f98a969c33c3b85976a1dc5b0aeece3e2"

View File

@ -1,7 +1,7 @@
[tool.poetry]
name = "honcho"
version = "0.0.1"
description = ""
version = "0.0.2"
description = "Honcho Server"
authors = ["Plastic Labs <hello@plasticlabs.ai>"]
readme = "README.md"
@ -13,6 +13,7 @@ python-dotenv = "^1.0.0"
sqlalchemy = "^2.0.25"
psycopg2-binary = "^2.9.9"
slowapi = "^0.1.8"
fastapi-pagination = "^0.12.14"
[build-system]

View File

@ -1,13 +1,14 @@
import json
from typing import Sequence, Optional
import uuid
from typing import Optional
from sqlalchemy import select
from sqlalchemy import select, Select
from sqlalchemy.orm import Session
from . import models, schemas
def get_session(db: Session, app_id: str, session_id: int, user_id: Optional[str] = None):
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)
if user_id is not None:
stmt = stmt.where(models.Session.user_id == user_id)
@ -18,33 +19,20 @@ def get_session(db: Session, app_id: str, session_id: int, user_id: Optional[str
def get_sessions(
db: Session, app_id: str, user_id: str, location_id: str | None = None
) -> Sequence[schemas.Session]:
) -> Select:
stmt = (
select(models.Session)
.where(models.Session.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 location_id is not None:
stmt = stmt.where(models.Session.location_id == location_id)
return db.scalars(stmt).all()
# filtered_by_user = db.query(models.Session).filter(
# models.Session.user_id == user_id
# )
# filtered_by_location = (
# filtered_by_user.filter(models.Session.location_id == location_id)
# if location_id is not None
# else filtered_by_user
# )
# return (
# filtered_by_location.filter(models.Session.is_active.is_(True))
# .order_by(models.Session.created_at.desc())
# .all()
# )
return stmt
# return db.scalars(stmt).all()
def create_session(
db: Session, app_id: str, user_id: str, session: schemas.SessionCreate
@ -61,11 +49,8 @@ def create_session(
return honcho_session
def update_session(db: Session, app_id: str, user_id: str, session_id: int, session_data: dict) -> bool:
# stmt = select(models.Session).where(models.Session.id == session_id).where(models.Session.user_id == user_id)
# honcho_session = db.scalars(stmt).one_or_none()
def update_session(db: Session, app_id: str, user_id: str, session_id: uuid.UUID, session_data: dict) -> bool:
honcho_session = get_session(db, app_id=app_id, session_id=session_id, user_id=user_id)
# honcho_session = db.get(models.Session, session_id)
if honcho_session is None:
raise ValueError("Session not found or does not belong to user")
honcho_session.session_data = json.dumps(session_data)
@ -74,7 +59,7 @@ def update_session(db: Session, app_id: str, user_id: str, session_id: int, sess
return honcho_session
def delete_session(db: Session, app_id: str, user_id: str, session_id: int) -> bool:
def delete_session(db: Session, app_id: str, user_id: str, session_id: uuid.UUID) -> bool:
stmt = (
select(models.Session)
.where(models.Session.id == session_id)
@ -82,7 +67,6 @@ def delete_session(db: Session, app_id: str, user_id: str, session_id: int) -> b
.where(models.Session.user_id == user_id)
)
honcho_session = db.scalars(stmt).one_or_none()
# honcho_session = db.get(models.Session, session_id)
if honcho_session is None:
return False
honcho_session.is_active = False
@ -91,7 +75,7 @@ def delete_session(db: Session, app_id: str, user_id: str, session_id: int) -> b
def create_message(
db: Session, message: schemas.MessageCreate, app_id: str, user_id: str, session_id: int
db: Session, message: schemas.MessageCreate, app_id: str, user_id: str, session_id: uuid.UUID
) -> models.Message:
honcho_session = get_session(db, app_id=app_id, session_id=session_id, user_id=user_id)
if honcho_session is None:
@ -109,36 +93,95 @@ def create_message(
def get_messages(
db: Session, app_id: str, user_id: str, session_id: int
) -> Sequence[schemas.Message]:
session = get_session(db, app_id=app_id, session_id=session_id, user_id=user_id)
if session is None:
raise ValueError("Session not found or does not belong to user")
stmt = select(models.Message).where(models.Message.session_id == session_id)
return db.scalars(stmt).all()
db: Session, app_id: str, user_id: str, session_id: uuid.UUID
) -> Select:
# session = get_session(db, app_id=app_id, session_id=session_id, user_id=user_id)
# if session is None:
# raise ValueError("Session not found or does not belong to user")
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)
.where(models.Message.session_id == session_id)
.order_by(models.Message.created_at)
)
return stmt
# return db.scalars(stmt).all()
# return (
# db.query(models.Message)
# .filter(models.Message.session_id == session_id)
# .all()
# )
def get_message(
db: Session, app_id: str, user_id: str, session_id: uuid.UUID, message_id: uuid.UUID
) -> Optional[models.Message]:
# session = get_session(db, app_id=app_id, session_id=session_id, user_id=user_id)
# if session is None:
# raise ValueError("Session not found or does not belong to user")
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)
.where(models.Message.session_id == session_id)
.where(models.Message.id == message_id)
# def get_metacognitions(db: Session, message_id: int):
# return (
# db.query(models.Metacognitions)
# .filter(models.Metacognitions.message_id == message_id)
# .all()
# )
)
return db.scalars(stmt).one_or_none()
# def create_metacognition(
# db: Session, metacognition: schemas.MetacognitionsCreate, message_id: int
# ):
# honcho_metacognition = models.Metacognitions(
# message_id=message_id,
# metacognition_type=metacognition.metacognition_type,
# content=metacognition.content,
# )
# db.add(honcho_metacognition)
# db.commit()
# db.refresh(honcho_metacognition)
# return honcho_metacognition
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,
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)
if message is None:
raise ValueError("Session not found or does not belong to user")
honcho_metamessage = models.Metamessage(
message_id=metamessage.message_id,
metamessage_type=metamessage.metamessage_type,
content=metamessage.content,
)
db.add(honcho_metamessage)
db.commit()
db.refresh(honcho_metamessage)
return honcho_metamessage

View File

@ -1,3 +1,4 @@
import uuid
from fastapi import Depends, FastAPI, HTTPException, APIRouter, Request
from typing import Optional
from sqlalchemy.orm import Session
@ -5,6 +6,9 @@ from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.middleware import SlowAPIMiddleware
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from fastapi_pagination import Page, add_pagination
from fastapi_pagination.ext.sqlalchemy import paginate
# import uvicorn
from . import crud, models, schemas
@ -17,7 +21,7 @@ app = FastAPI()
router = APIRouter(prefix="/apps/{app_id}/users/{user_id}")
# Create a Limiter instance
limiter = Limiter(key_func=get_remote_address, default_limits=["5/minute"])
limiter = Limiter(key_func=get_remote_address, default_limits=["100/minute"])
# Add SlowAPI middleware to the application
app.state.limiter = limiter
@ -25,6 +29,8 @@ app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
app.add_middleware(SlowAPIMiddleware)
add_pagination(app)
def get_db():
"""FastAPI Dependency Generator for Database"""
db = SessionLocal()
@ -37,7 +43,7 @@ def get_db():
# Session Routes
########################################################
@router.get("/sessions", response_model=list[schemas.Session])
@router.get("/sessions", response_model=Page[schemas.Session])
def get_sessions(request: Request, app_id: str, user_id: str, location_id: Optional[str] = None, db: Session = Depends(get_db)):
"""Get All Sessions for a User
@ -50,9 +56,11 @@ def get_sessions(request: Request, app_id: str, user_id: str, location_id: Optio
list[schemas.Session]: List of Session objects
"""
if location_id is not None:
return crud.get_sessions(db, app_id=app_id, user_id=user_id, location_id=location_id)
return crud.get_sessions(db, app_id=app_id, user_id=user_id)
# if location_id is not None:
# return paginate(db, crud.get_sessions(db, app_id=app_id, user_id=user_id, location_id=location_id))
# return crud.get_sessions(db, app_id=app_id, user_id=user_id, location_id=location_id)
return paginate(db, crud.get_sessions(db, app_id=app_id, user_id=user_id, location_id=location_id))
# return crud.get_sessions(db, app_id=app_id, user_id=user_id)
@router.post("/sessions", response_model=schemas.Session)
@ -70,14 +78,15 @@ def create_session(
schemas.Session: The Session object of the new Session
"""
return crud.create_session(db, app_id=app_id, user_id=user_id, session=session)
value = crud.create_session(db, app_id=app_id, user_id=user_id, session=session)
return value
@router.put("/sessions/{session_id}", response_model=schemas.Session)
def update_session(
request: Request,
app_id: str,
user_id: str,
session_id: int,
session_id: uuid.UUID,
session: schemas.SessionUpdate,
db: Session = Depends(get_db),
):
@ -105,7 +114,7 @@ def delete_session(
request: Request,
app_id: str,
user_id: str,
session_id: int,
session_id: uuid.UUID,
db: Session = Depends(get_db),
):
"""Delete a session by marking it as inactive
@ -129,7 +138,7 @@ def delete_session(
raise HTTPException(status_code=404, detail="Session not found")
@router.get("/sessions/{session_id}", response_model=schemas.Session)
def get_session(request: Request, app_id: str, user_id: str, session_id: int, db: Session = Depends(get_db)):
def get_session(request: Request, app_id: str, user_id: str, session_id: uuid.UUID, db: Session = Depends(get_db)):
"""Get a specific session for a user by ID
Args:
@ -160,7 +169,7 @@ def create_message_for_session(
request: Request,
app_id: str,
user_id: str,
session_id: int,
session_id: uuid.UUID,
message: schemas.MessageCreate,
db: Session = Depends(get_db),
):
@ -186,13 +195,13 @@ def create_message_for_session(
@router.get(
"/sessions/{session_id}/messages",
response_model=list[schemas.Message]
response_model=Page[schemas.Message]
)
def get_messages_for_session(
request: Request,
app_id: str,
user_id: str,
session_id: int,
session_id: uuid.UUID,
db: Session = Depends(get_db),
):
"""Get all messages for a session
@ -210,40 +219,122 @@ def get_messages_for_session(
"""
try:
return crud.get_messages(db, app_id=app_id, user_id=user_id, session_id=session_id)
return paginate(db, crud.get_messages(db, app_id=app_id, user_id=user_id, session_id=session_id))
except ValueError:
raise HTTPException(status_code=404, detail="Session not found")
@router.get(
"sessions/{session_id}/messages/{message_id}",
response_model=schemas.Message
)
def get_message(
request: Request,
app_id: str,
user_id: str,
session_id: uuid.UUID,
message_id: uuid.UUID,
db: Session = Depends(get_db),
):
"""
"""
honcho_message = crud.get_message(db, app_id=app_id, session_id=session_id, user_id=user_id, message_id=message_id)
if honcho_message is None:
raise HTTPException(status_code=404, detail="Session not found")
return honcho_message
app.include_router(router)
########################################################
# Metacognition Routes
########################################################
# @app.get(
# "/users/{user_id}/sessions/{session_id}/messages/{message_id}/metacognitions/",
# response_model=list[schemas.Metacognitions],
# )
# def get_metacognitions_for_message(
# user_id: str,
# session_id: int,
# message_id: int,
# db: Session = Depends(get_db),
# ):
# return crud.get_metacognitions(db, message_id)
@router.post(
"/sessions/{session_id}/metamessages",
response_model=schemas.Metamessage
)
def create_metamessage(
request: Request,
app_id: str,
user_id: str,
session_id: uuid.UUID,
metamessage: schemas.MetamessageCreate,
db: Session = Depends(get_db),
):
"""Adds a message to a session
Args:
app_id (str): The ID of the app representing the client application using honcho
user_id (str): The User ID representing the user, managed by the user
session_id (int): The ID of the Session to add the message to
message (schemas.MessageCreate): The Message object to add containing the message content and type
Returns:
schemas.Message: The Message object of the added message
Raises:
HTTPException: If the session is not found
"""
print("=======================")
print(request)
print("=======================")
try:
return crud.create_metamessage(db, metamessage=metamessage, app_id=app_id, user_id=user_id, session_id=session_id)
except ValueError:
raise HTTPException(status_code=404, detail="Session not found")
@router.get(
"/sessions/{session_id}/metamessages",
response_model=Page[schemas.Metamessage]
)
def get_metamessages(
request: Request,
app_id: str,
user_id: str,
session_id: uuid.UUID,
message_id: Optional[uuid.UUID] = None,
metamessage_type: Optional[str] = None,
db: Session = Depends(get_db),
):
"""Get all messages for a session
Args:
app_id (str): The ID of the app representing the client application using honcho
user_id (str): The User ID representing the user, managed by the user
session_id (int): The ID of the Session to retrieve
Returns:
list[schemas.Message]: List of Message objects
Raises:
HTTPException: If the session is not found
"""
try:
return paginate(db, crud.get_metamessages(db, app_id=app_id, user_id=user_id, session_id=session_id, message_id=message_id, metamessage_type=metamessage_type))
except ValueError:
raise HTTPException(status_code=404, detail="Session not found")
@router.get("/sessions/{session_id}/metamessages/{metamessage_id}", response_model=schemas.Metamessage)
def get_metamessage(request: Request, app_id: str, user_id: str, session_id: uuid.UUID, message_id: uuid.UUID, metamessage_id: uuid.UUID, db: Session = Depends(get_db)):
"""Get a specific session for a user by ID
Args:
app_id (str): The ID of the app representing the client application using honcho
user_id (str): The User ID representing the user, managed by the user
session_id (int): The ID of the Session to retrieve
Returns:
schemas.Session: The Session object of the requested Session
Raises:
HTTPException: If the session is not found
"""
honcho_metamessage = crud.get_metamessage(db, app_id=app_id, session_id=session_id, user_id=user_id, message_id=message_id, metamessage_id=metamessage_id)
if honcho_metamessage is None:
raise HTTPException(status_code=404, detail="Session not found")
return honcho_metamessage
# @app.post(
# "/users/{user_id}/sessions/{session_id}/messages/{message_id}/metacognitions/",
# response_model=schemas.Metacognitions,
# )
# def create_metacognition_for_message(
# user_id: str,
# session_id: int,
# message_id: int,
# metacognition: schemas.MetacognitionsCreate,
# db: Session = Depends(get_db),
# ):
# return crud.create_metacognition(db, metacognition, message_id)
app.include_router(router)

View File

@ -1,4 +1,5 @@
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, DateTime
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, DateTime, Uuid
import uuid
import datetime
from sqlalchemy.orm import relationship, Mapped, mapped_column
@ -7,18 +8,12 @@ from .db import Base
class Session(Base):
__tablename__ = "sessions"
# id = Column(Integer, primary_key=True, index=True, autoincrement=True)
# user_id = Column(String, index=True)
# location_id = Column(String, index=True)
# is_active = Column(Boolean, default=True)
# session_data = Column(String)
# created_at = Column(DateTime, default=datetime.datetime.utcnow)
id: Mapped[int] = mapped_column(primary_key=True, index=True, autoincrement=True)
app_id: Mapped[str] = mapped_column(index=True)
user_id: Mapped[str] = mapped_column(index=True)
location_id: Mapped[str] = mapped_column(index=True)
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)
is_active: Mapped[bool] = mapped_column(default=True)
session_data: Mapped[str]
session_data: Mapped[str]
created_at: Mapped[datetime.datetime] = mapped_column(default=datetime.datetime.utcnow)
messages = relationship("Message", back_populates="session")
@ -28,33 +23,27 @@ class Session(Base):
class Message(Base):
__tablename__ = "messages"
# id = Column(Integer, primary_key=True, index=True, autoincrement=True)
# session_id = Column(Integer, ForeignKey("sessions.id"))
# is_user = Column(Boolean)
# content = Column(String)
id: Mapped[int] = mapped_column(primary_key=True, index=True, autoincrement=True)
session_id: Mapped[int] = 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"))
is_user: Mapped[bool]
content: Mapped[str] # TODO add a max message length
content: Mapped[str] = mapped_column(String(65535))
created_at: Mapped[datetime.datetime] = mapped_column(default=datetime.datetime.utcnow)
session = relationship("Session", back_populates="messages")
metamessages = relationship("Metamessages", back_populates="message")
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:]})"
# TODO: add metamessages data to messages
class Metamessages(Base):
class Metamessage(Base):
__tablename__ = "metamessages"
id: Mapped[int] = mapped_column(primary_key=True, index=True, autoincrement=True)
metamessage_type: Mapped[str] # TODO add a max metamessages type length
content: Mapped[str]
# id = Column(Integer, primary_key=True, index=True, autoincrement=True)
message_id = Column(Integer, ForeignKey("messages.id"))
# metacognition_type = Column(String, index=True)
# content = Column(String)
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"))
message = relationship("Message", back_populates="metamessages")
created_at: Mapped[datetime.datetime] = mapped_column(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:]})"

View File

@ -1,4 +1,6 @@
from pydantic import BaseModel
import datetime
import uuid
class MessageBase(BaseModel):
@ -11,8 +13,9 @@ class MessageCreate(MessageBase):
class Message(MessageBase):
session_id: int
id: int
session_id: uuid.UUID
id: uuid.UUID
created_at: datetime.datetime
class Config:
orm_mode = True
@ -32,29 +35,32 @@ class SessionUpdate(SessionBase):
class Session(SessionBase):
id: int
messages: list[Message]
id: uuid.UUID
# messages: list[Message]
is_active: bool
user_id: str
location_id: str
app_id: str
session_data: str
created_at: datetime.datetime
class Config:
orm_mode = True
class MetamessagesBase(BaseModel):
class MetamessageBase(BaseModel):
metamessage_type: str
content: str
class MetamessagesCreate(MetamessagesBase):
pass
class MetamessageCreate(MetamessageBase):
message_id: uuid.UUID
class Metamessages(MetamessagesBase):
id: int
class Metamessage(MetamessageBase):
id: uuid.UUID
message_id: uuid.UUID
created_at: datetime.datetime
class Config:
orm_mode = True

View File

@ -9,7 +9,8 @@ from honcho import Client as HonchoClient
app_id = str(uuid4())
honcho = HonchoClient(app_id=app_id)
# 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
responses = ["Fake LLM Response :)"]
llm = FakeListChatModel(responses=responses)
@ -35,10 +36,10 @@ def chat():
while True:
user_input = input("User: ")
if user_input == "exit":
session.delete()
session.close()
break
user_message = HumanMessage(content=user_input)
history = session.get_messages()
history = list(session.get_messages_generator())
langchain_history = langchain_message_converter(history)
prompt = ChatPromptTemplate.from_messages(
[system, *langchain_history, user_message]

View File

@ -168,7 +168,7 @@ tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "p
name = "certifi"
version = "2023.11.17"
description = "Python package for providing Mozilla's CA Bundle."
category = "dev"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
@ -180,7 +180,7 @@ files = [
name = "charset-normalizer"
version = "3.3.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "dev"
category = "main"
optional = false
python-versions = ">=3.7.0"
files = [
@ -451,11 +451,28 @@ files = [
docs = ["Sphinx", "furo"]
test = ["objgraph", "psutil"]
[[package]]
name = "honcho-ai"
version = "0.0.1"
description = "Python Client SDK for Honcho"
category = "main"
optional = false
python-versions = "^3.10"
files = []
develop = true
[package.dependencies]
requests = "^2.31.0"
[package.source]
type = "directory"
url = "../../sdk"
[[package]]
name = "idna"
version = "3.6"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "dev"
category = "main"
optional = false
python-versions = ">=3.5"
files = [
@ -939,7 +956,7 @@ files = [
name = "requests"
version = "2.31.0"
description = "Python HTTP for Humans."
category = "dev"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -1104,7 +1121,7 @@ typing-extensions = ">=3.7.4"
name = "urllib3"
version = "2.2.0"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "dev"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@ -1225,4 +1242,4 @@ multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "ac8bd006b50a14281b2705dc33626123173155c770b0150a19b25034efa251f3"
content-hash = "d254e9446dc0f782b0860a860fa98f0922cf7f6ab547a3111027f4642fe1ce5b"

View File

@ -7,7 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = "^3.11"
honcho-ai = {path = "../../sdk"}
honcho-ai = {path = "../../sdk", develop = true}
[tool.poetry.group.dev.dependencies]
langchain = "^0.1.0"

View File

@ -13,7 +13,9 @@ intents.message_content = True
app_id = str(uuid4())
honcho = HonchoClient(app_id=app_id, "http://localhost:8000")
# 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
bot = discord.Bot(intents=intents)
@ -30,7 +32,7 @@ async def on_message(message):
user_id = f"discord_{str(message.author.id)}"
location_id = str(message.channel.id)
sessions = honcho.get_sessions(user_id, location_id)
sessions = list(honcho.get_sessions_generator(user_id, location_id))
if len(sessions) > 0:
session = sessions[0]
else:
@ -49,8 +51,8 @@ async def on_message(message):
async def restart(ctx):
user_id = f"discord_{str(ctx.author.id)}"
location_id = str(ctx.channel_id)
sessions = honcho.get_sessions(user_id, location_id)
sessions[0].delete() if len(sessions) > 0 else None
sessions = list(honcho.get_sessions_generator(user_id, location_id))
sessions[0].close() if len(sessions) > 0 else None
await ctx.respond(
"Great! The conversation has been restarted. What would you like to talk about?"

View File

@ -124,6 +124,27 @@ files = [
[package.dependencies]
frozenlist = ">=1.1.0"
[[package]]
name = "anyio"
version = "4.2.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"},
{file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"},
]
[package.dependencies]
idna = ">=2.8"
sniffio = ">=1.1"
[package.extras]
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 = "async-timeout"
version = "4.0.3"
@ -158,14 +179,14 @@ tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "p
[[package]]
name = "certifi"
version = "2023.11.17"
version = "2024.2.2"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"},
{file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"},
{file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
{file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
]
[[package]]
@ -355,22 +376,82 @@ files = [
{file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"},
]
[[package]]
name = "h11"
version = "0.14.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
]
[[package]]
name = "honcho-ai"
version = "0.0.0.dev1"
version = "0.0.2"
description = "Python Client SDK for Honcho"
category = "main"
optional = false
python-versions = "^3.10"
files = []
develop = false
develop = true
[package.dependencies]
httpx = "^0.26.0"
requests = "^2.31.0"
[package.source]
type = "directory"
url = "../../sdk"
url = "../../../sdk"
[[package]]
name = "httpcore"
version = "1.0.2"
description = "A minimal low-level HTTP client."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"},
{file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"},
]
[package.dependencies]
certifi = "*"
h11 = ">=0.13,<0.15"
[package.extras]
asyncio = ["anyio (>=4.0,<5.0)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (>=1.0.0,<2.0.0)"]
trio = ["trio (>=0.22.0,<0.23.0)"]
[[package]]
name = "httpx"
version = "0.26.0"
description = "The next generation HTTP client."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"},
{file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"},
]
[package.dependencies]
anyio = "*"
certifi = "*"
httpcore = ">=1.0.0,<2.0.0"
idna = "*"
sniffio = "*"
[package.extras]
brotli = ["brotli", "brotlicffi"]
cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (>=1.0.0,<2.0.0)"]
[[package]]
name = "idna"
@ -386,86 +467,102 @@ files = [
[[package]]
name = "multidict"
version = "6.0.4"
version = "6.0.5"
description = "multidict implementation"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"},
{file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"},
{file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"},
{file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"},
{file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"},
{file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"},
{file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"},
{file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"},
{file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"},
{file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"},
{file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"},
{file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"},
{file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"},
{file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"},
{file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"},
{file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"},
{file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"},
{file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"},
{file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"},
{file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"},
{file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"},
{file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"},
{file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"},
{file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"},
{file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"},
{file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"},
{file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"},
{file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"},
{file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"},
{file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"},
{file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"},
{file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"},
{file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"},
{file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"},
{file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"},
{file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"},
{file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"},
{file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"},
{file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"},
{file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"},
{file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"},
{file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"},
{file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"},
{file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"},
{file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"},
{file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"},
{file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"},
{file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"},
{file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"},
{file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"},
{file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"},
{file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"},
{file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"},
{file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"},
{file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"},
{file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"},
{file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"},
{file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"},
{file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"},
{file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"},
{file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"},
{file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"},
{file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"},
{file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"},
{file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"},
{file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"},
{file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"},
{file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"},
{file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"},
{file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"},
{file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"},
{file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"},
{file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"},
{file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"},
{file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"},
{file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"},
{file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"},
{file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"},
{file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"},
{file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"},
{file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"},
{file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"},
{file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"},
{file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"},
{file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"},
{file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"},
{file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"},
{file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"},
{file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"},
{file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"},
{file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"},
{file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"},
{file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"},
{file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"},
{file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"},
{file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"},
{file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"},
{file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"},
{file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"},
{file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"},
{file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"},
{file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"},
{file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"},
{file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"},
]
[[package]]
@ -525,20 +622,33 @@ urllib3 = ">=1.21.1,<3"
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "sniffio"
version = "1.3.0"
description = "Sniff out which async library your code is running under"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
{file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
]
[[package]]
name = "urllib3"
version = "2.1.0"
version = "2.2.0"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"},
{file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"},
{file = "urllib3-2.2.0-py3-none-any.whl", hash = "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"},
{file = "urllib3-2.2.0.tar.gz", hash = "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20"},
]
[package.extras]
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
@ -649,4 +759,4 @@ multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "159adce2e4b4e405c0e57a5b9a27ddc384184dd308d6b12ddb5db6fbe81c3c6b"
content-hash = "f31c071455001b66fe72eb743b16c64f108172c4314475b772e5cbd18942d0dc"

View File

@ -9,7 +9,7 @@ readme = "README.md"
python = "^3.11"
py-cord = "^2.4.1"
python-dotenv = "^1.0.0"
honcho-ai = {path = "../../sdk"}
honcho-ai = {path = "../../../sdk", develop = true}
[build-system]

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,42 @@
# Simple Roast Bot
The goal of this repo is to demonstrate how to deploy an LLM application using Honcho to manage user data. Here we've implemented a simple Discord bot that interacts with OpenAI's GPT-3.5-Turbo model via LangChain. Oh, and also, it's prompted to roast you.
***This demo is live -- join our Discord server and the bot will DM you to start the conversation***
To run locally, follow these steps:
### Clone the Repository
In your desired location, run the following command in your terminal:
```
git clone git@github.com:plastic-labs/honcho.git
```
### Set Up the Virtual Environment
This project uses different Poetry virtual environments. If you're unfamiliar, take a look at their docs [here](https://python-poetry.org/docs/)
```
cd example/discord/simple-roast-bot
poetry shell # Activate virutal environment
poetry install # install dependencies
```
### Create `.env` File
Copy the `.env.template` file to a `.env` file and specify the `BOT_TOKEN` and `OPENAI_API_KEY`. If you've never built a Discord bot before, check out this [`py-cord` guide](https://guide.pycord.dev/getting-started/creating-your-first-bot) to learn more about how to get a `BOT_TOKEN`. You can generate an `OPENAI_API_KEY` in the [OpenAI developer platform](https://platform.openai.com/docs/overview).
```
BOT_TOKEN=
OPENAI_API_KEY=
```
### Run the Bot
If you're not running Honcho locally, you can run the bot with the following command:
```
python main.py
```
If you are interested in running Honcho locally, follow the setup instructions at the root of this repo.

View File

@ -0,0 +1,90 @@
import os
# from uuid import uuid4
import discord
from dotenv import load_dotenv
from typing import List
from langchain_openai import ChatOpenAI
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
load_dotenv()
intents = discord.Intents.default()
intents.messages = True
intents.message_content = True
# app_id = str(uuid4())
app_id = 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
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}")
])
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:
if message.is_user:
new_messages.append(HumanMessage(content=message.content))
else:
new_messages.append(AIMessage(content=message.content))
return new_messages
@bot.event
async def on_ready():
print(f'We have logged in as {bot.user}')
@bot.event
async def on_message(message):
if message.author == bot.user:
return
user_id = f"discord_{str(message.author.id)}"
location_id=str(message.channel.id)
sessions = list(honcho.get_sessions_generator(user_id, location_id))
if len(sessions) > 0:
session = sessions[0]
else:
session = honcho.create_session(user_id, location_id)
history = list(session.get_messages_generator())
chat_history = langchain_message_converter(history)
inp = message.content
session.create_message(is_user=True, content=inp)
async with message.channel.typing():
response = await chain.ainvoke({"chat_history": chat_history, "input": inp})
await message.channel.send(response)
session.create_message(is_user=False, content=response)
@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))
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"])

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
[tool.poetry]
name = "simple-roast-bot"
version = "0.1.0"
description = "Simple Discord bot with Honcho storage backend (that will roast you)"
authors = ["vintro <vince@plasticlabs.ai>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.11"
py-cord = "^2.4.1"
python-dotenv = "^1.0.0"
langchain-core = "^0.1.12"
langchain-openai = "^0.0.2.post1"
honcho-ai = {path = "../../../sdk", develop = true}
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

18
scripts/syncronizer.py Normal file
View File

@ -0,0 +1,18 @@
import os
import re
# Open the source file
this_dir = os.path.dirname(os.path.abspath(__file__))
source_file_path = os.path.join(this_dir, "../sdk/honcho/client.py")
with open(source_file_path, "r") as source_file:
source_code = source_file.read()
# Use regex to remove async mentions
sync_code = re.sub(r"async\s", "", source_code)
sync_code = re.sub(r"await\s", "", sync_code)
sync_code = re.sub(r"Async", "", sync_code)
# Write the modified code to the destination file
destination_file_path = os.path.join(this_dir, "../sdk/honcho/sync_client.py")
with open(destination_file_path, "w") as destination_file:
destination_file.write(sync_code)

6
sdk/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"python.analysis.typeCheckingMode": "basic",
"python.testing.pytestArgs": ["tests"],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}

View File

@ -6,6 +6,31 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [0.0.2] — 2024-02-08
### Added
* Async client
* Metamessages introduced
* Paginated results for get requests
* `created_at` field added to Messages and Metamessages
* added singular `get_message` method
* Size limits for messages and string fields
### Changed
* Default rate limit of 100/minutes
* Changed default ID type to use UUIDs
* `session.delete()` is now `session.close()`
* replace `requests` for `httpx`
### Removed
* Removed messages from session response model
## [0.0.1] — 2024-02-01
### Added

View File

@ -1,2 +1,4 @@
from .client import Client
from .client import AsyncClient, AsyncSession, AsyncGetSessionPage, AsyncGetMessagePage, AsyncGetMetamessagePage
from .sync_client import Client, Session, GetSessionPage, GetMessagePage, GetMetamessagePage
from .schemas import Message, Metamessage
from .cache import LRUCache

View File

@ -1,33 +1,161 @@
import json
from typing import Dict
import requests
import uuid
from typing import Dict, Optional
import httpx
from .schemas import Message, Metamessage
class AsyncGetPage:
"""Base class for receiving Paginated API results"""
def __init__(self, response: Dict) -> None:
"""Constructor for Page with relevant information about the results and pages
Args:
response (Dict): Response from API with pagination information
"""
self.total = response["total"]
self.page = response["page"]
self.page_size = response["size"]
self.pages = response["pages"]
self.items =[]
async def next(self):
"""Shortcut method to Get the next page of results"""
pass
class AsyncGetSessionPage(AsyncGetPage):
"""Paginated Results for Get Session Requests"""
def __init__(self, client, options: Dict, response: Dict):
"""Constructor for Page Result from Session Get Request
Args:
client (AsyncClient): Honcho Client
options (Dict): Options for the request used mainly for next() to filter queries. The two parameters available are user_id which is required and location_id which is optional
response (Dict): Response from API with pagination information
"""
super().__init__(response)
self.client = client
self.user_id = options["user_id"]
self.location_id = options["location_id"]
self.items = [
AsyncSession(
client=client,
id=session["id"],
user_id=session["user_id"],
location_id=session["location_id"],
is_active=session["is_active"],
session_data=session["session_data"],
)
for session in response["items"]
]
async def next(self):
"""Get the next page of results
Returns:
AsyncGetSessionPage | None: Next Page of Results or None if there are no more sessions to retreive from a query
"""
if self.page >= self.pages:
return None
return await self.client.get_sessions(self.user_id, self.location_id, self.page + 1, self.page_size)
class AsyncGetMessagePage(AsyncGetPage):
"""Paginated Results for Get Session Requests"""
def __init__(self, session, response: Dict):
"""Constructor for Page Result from Session Get Request
Args:
session (AsyncSession): Session the returned messages are associated with
response (Dict): Response from API with pagination information
"""
super().__init__(response)
self.session = session
self.items = [
Message(
session_id=session.id,
id=message["id"],
is_user=message["is_user"],
content=message["content"],
created_at=message["created_at"],
)
for message in response["items"]
]
async def next(self):
"""Get the next page of results
Returns:
AsyncGetMessagePage | None: Next Page of Results or None if there are no more messages to retreive from a query
"""
if self.page >= self.pages:
return None
return await self.session.get_messages((self.page + 1), self.page_size)
class AsyncGetMetamessagePage(AsyncGetPage):
def __init__(self, session, options: Dict, response: Dict) -> None:
"""Constructor for Page Result from Metamessage Get Request
Args:
session (AsyncSession): Session the returned messages are associated with
options (Dict): Options for the request used mainly for next() to filter queries. The two parameters available are message_id and metamessage_type which are both required
response (Dict): Response from API with pagination information
"""
super().__init__(response)
self.session = session
self.message_id = options["message_id"]
self.metamessage_type = options["metamessage_type"]
self.items = [
Metamessage(
id=metamessage["id"],
message_id=metamessage["message_id"],
metamessage_type=metamessage["metamessage_type"],
content=metamessage["content"],
created_at=metamessage["created_at"],
)
for metamessage in response["items"]
]
async def next(self):
"""Get the next page of results
Returns:
AsyncGetMetamessagePage | None: Next Page of Results or None if there are no more metamessages to retreive from a query
"""
if self.page >= self.pages:
return None
return await self.session.get_metamessages(metamessage_type=self.metamessage_type, message=self.message_id, page=(self.page + 1), page_size=self.page_size)
class Client:
class AsyncClient:
"""Honcho API Client Object"""
def __init__(self, app_id: str, base_url: str = "https://demo.honcho.dev"):
"""Constructor for Client"""
self.base_url = base_url # Base URL for the instance of the Honcho API
self.app_id = app_id # Representing ID of the client application
self.client = httpx.AsyncClient()
@property
def common_prefix(self):
"""Shorcut for common API prefix. made a property to prevent tampering"""
return f"{self.base_url}/apps/{self.app_id}"
def get_session(self, user_id: str, session_id: int):
async def get_session(self, user_id: str, session_id: uuid.UUID):
"""Get a specific session for a user by ID
Args:
user_id (str): The User ID representing the user, managed by the user
session_id (int): The ID of the Session to retrieve
session_id (uuid.UUID): The ID of the Session to retrieve
Returns:
Dict: The Session object of the requested Session
AsyncSession: The Session object of the requested Session
"""
url = f"{self.common_prefix}/users/{user_id}/sessions/{session_id}"
response = requests.get(url)
response = await self.client.get(url)
response.raise_for_status()
data = response.json()
return Session(
return AsyncSession(
client=self,
id=data["id"],
user_id=data["user_id"],
@ -36,34 +164,57 @@ class Client:
session_data=data["session_data"],
)
def get_sessions(self, user_id: str, location_id: str | None = None):
"""Return sessions associated with a user
async def get_sessions(self, user_id: str, location_id: Optional[str] = None, page: int = 1, page_size: int = 50):
"""Return sessions associated with a user paginated
Args:
user_id (str): The User ID representing the user, managed by the user
location_id (str, optional): Optional Location ID representing the location of a session
page (int, optional): The page of results to return
page_size (int, optional): The number of results to return
Returns:
AsyncGetSessionPage: Page or results for get_sessions query
"""
url = f"{self.common_prefix}/users/{user_id}/sessions?page={page}&size={page_size}" + (
f"&location_id={location_id}" if location_id else ""
)
response = await self.client.get(url)
response.raise_for_status()
data = response.json()
options = {
"location_id": location_id,
"user_id": user_id
}
return AsyncGetSessionPage(self, options, data)
async def get_sessions_generator(self, user_id: str, location_id: Optional[str] = None):
"""Shortcut Generator for get_sessions. Generator to iterate through all sessions for a user in an app
Args:
user_id (str): The User ID representing the user, managed by the user
location_id (str, optional): Optional Location ID representing the location of a session
Returns:
list[Dict]: List of Session objects
Yields:
AsyncSession: The Session object of the requested Session
"""
url = f"{self.common_prefix}/users/{user_id}/sessions" + (
f"?location_id={location_id}" if location_id else ""
)
response = requests.get(url)
return [
Session(
client=self,
id=session["id"],
user_id=session["user_id"],
location_id=session["location_id"],
is_active=session["is_active"],
session_data=session["session_data"],
)
for session in response.json()
]
page = 1
page_size = 50
get_session_response = await self.get_sessions(user_id, location_id, page, page_size)
while True:
# get_session_response = self.get_sessions(user_id, location_id, page, page_size)
for session in get_session_response.items:
yield session
def create_session(
new_sessions = await get_session_response.next()
if not new_sessions:
break
get_session_response = new_sessions
async def create_session(
self, user_id: str, location_id: str = "default", session_data: Dict = {}
):
"""Create a session for a user
@ -74,14 +225,15 @@ class Client:
session_data (Dict, optional): Optional session metadata
Returns:
Dict: The Session object of the new Session`
AsyncSession: The Session object of the new Session
"""
data = {"location_id": location_id, "session_data": session_data}
url = f"{self.common_prefix}/users/{user_id}/sessions"
response = requests.post(url, json=data)
response = await self.client.post(url, json=data)
response.raise_for_status()
data = response.json()
return Session(
return AsyncSession(
self,
id=data["id"],
user_id=user_id,
@ -91,11 +243,13 @@ class Client:
)
class Session:
class AsyncSession:
"""Represents a single session for a user in an app"""
def __init__(
self,
client: Client,
id: int,
client: AsyncClient,
id: uuid.UUID,
user_id: str,
location_id: str,
session_data: dict | str,
@ -103,6 +257,7 @@ class Session:
):
"""Constructor for Session"""
self.base_url = client.base_url
self.client = client.client
self.app_id = client.app_id
self.id = id
self.user_id = user_id
@ -114,16 +269,19 @@ class Session:
@property
def common_prefix(self):
"""Shortcut for common API prefix. made a property to prevent tampering"""
return f"{self.base_url}/apps/{self.app_id}"
def __str__(self):
return f"Session(id={self.id}, app_id={self.app_id}, user_id={self.user_id}, location_id={self.location_id}, session_data={self.session_data}, is_active={self.is_active})"
"""String representation of Session"""
return f"AsyncSession(id={self.id}, app_id={self.app_id}, user_id={self.user_id}, location_id={self.location_id}, session_data={self.session_data}, is_active={self.is_active})"
@property
def is_active(self):
"""Returns whether the session is active - made property to prevent tampering"""
return self._is_active
def create_message(self, is_user: bool, content: str):
async def create_message(self, is_user: bool, content: str):
"""Adds a message to the session
Args:
@ -131,18 +289,111 @@ class Session:
content (str): The content of the message
Returns:
Dict: The Message object of the added message
Message: The Message object of the added message
"""
if not self.is_active:
raise Exception("Session is inactive")
data = {"is_user": is_user, "content": content}
url = f"{self.common_prefix}/users/{self.user_id}/sessions/{self.id}/messages"
response = requests.post(url, json=data)
response = await self.client.post(url, json=data)
response.raise_for_status()
data = response.json()
return Message(self, id=data["id"], is_user=is_user, content=content)
return Message(session_id=self.id, id=data["id"], is_user=is_user, content=content, created_at=data["created_at"])
def get_messages(self):
async def get_message(self, message_id: uuid.UUID) -> Message:
"""Get a specific message for a session based on ID
Args:
message_id (uuid.UUID): The ID of the Message to retrieve
Returns:
Message: The Message object
"""
url = f"{self.common_prefix}/users/{self.user_id}/sessions/{self.id}/messages/{message_id}"
response = await self.client.get(url)
response.raise_for_status()
data = response.json()
return Message(session_id=self.id, id=data["id"], is_user=data["is_user"], content=data["content"], created_at=data["created_at"])
async def get_messages(self, page: int = 1, page_size: int = 50) -> AsyncGetMessagePage:
"""Get all messages for a session
Args:
page (int, optional): The page of results to return
page_size (int, optional): The number of results to return per page
Returns:
AsyncGetMessagePage: Page of Message objects
"""
url = f"{self.common_prefix}/users/{self.user_id}/sessions/{self.id}/messages?page={page}&size={page_size}"
response = await self.client.get(url)
response.raise_for_status()
data = response.json()
return AsyncGetMessagePage(self, data)
async def get_messages_generator(self):
"""Shortcut Generator for get_messages. Generator to iterate through all messages for a session in an app
Yields:
Message: The Message object of the next Message
"""
page = 1
page_size = 50
get_messages_page= await self.get_messages(page, page_size)
while True:
# get_session_response = self.get_sessions(user_id, location_id, page, page_size)
for message in get_messages_page.items:
yield message
new_messages = await get_messages_page.next()
if not new_messages:
break
get_messages_page = new_messages
async def create_metamessage(self, message: Message, metamessage_type: str, content: str):
"""Adds a metamessage to a session and links it to a specific message
Args:
message (Message): A message to associate the metamessage with
metamessage_type (str): The type of the metamessage arbitrary itentifier
content (str): The content of the metamessage
Returns:
Metamessage: The Metamessage object of the added metamessage
"""
if not self.is_active:
raise Exception("Session is inactive")
data = {"metamessage_type": metamessage_type, "content": content, "message_id": message.id}
url = f"{self.common_prefix}/users/{self.user_id}/sessions/{self.id}/metamessages"
response = await self.client.post(url, json=data)
response.raise_for_status()
data = response.json()
return Metamessage(id=data["id"], message_id=message.id, metamessage_type=metamessage_type, content=content, created_at=data["created_at"])
async def get_metamessage(self, metamessage_id: uuid.UUID) -> Metamessage:
"""Get a specific metamessage
Args:
message_id (uuid.UUID): The ID of the Message to retrieve
Returns:
Message: The Message object
"""
url = f"{self.common_prefix}/users/{self.user_id}/sessions/{self.id}/metamessages/{metamessage_id}"
response = await self.client.get(url)
response.raise_for_status()
data = response.json()
return Metamessage(id=data["id"], message_id=data["message_id"], metamessage_type=data["metamessage_type"], content=data["content"], created_at=data["created_at"])
async def get_metamessages(self, metamessage_type: Optional[str] = None, message: Optional[Message] = None, page: int = 1, page_size: int = 50) -> AsyncGetMetamessagePage:
"""Get all messages for a session
Args:
@ -153,24 +404,51 @@ class Session:
list[Dict]: List of Message objects
"""
url = f"{self.common_prefix}/users/{self.user_id}/sessions/{self.id}/messages"
response = requests.get(url)
url = f"{self.common_prefix}/users/{self.user_id}/sessions/{self.id}/metamessages?page={page}&size={page_size}"
if metamessage_type:
url += f"&metamessage_type={metamessage_type}"
if message:
url += f"&message_id={message.id}"
response = await self.client.get(url)
response.raise_for_status()
data = response.json()
return [
Message(
self,
id=message["id"],
is_user=message["is_user"],
content=message["content"],
)
for message in data
]
def update(self, session_data: Dict):
"""Update the metadata of a session
options = {
"metamessage_type": metamessage_type,
"message_id": message.id if message else None
}
return AsyncGetMetamessagePage(self, options, data)
async def get_metamessages_generator(self, metamessage_type: Optional[str] = None, message: Optional[Message] = None):
"""Shortcut Generator for get_metamessages. Generator to iterate through all metamessages for a session in an app
Args:
session_data (Dict): The Session object containing any new metadata
metamessage_type (str, optional): Optional Metamessage type to filter by
message (Message, optional): Optional Message to filter by
Yields:
Metamessage: The next Metamessage object of the requested query
"""
page = 1
page_size = 50
get_metamessages_page = await self.get_metamessages(metamessage_type=metamessage_type, message=message, page=page, page_size=page_size)
while True:
# get_session_response = self.get_sessions(user_id, location_id, page, page_size)
for metamessage in get_metamessages_page.items:
yield metamessage
new_messages = await get_metamessages_page.next()
if not new_messages:
break
get_metamessages_page = new_messages
async def update(self, session_data: Dict):
"""Update the session_data of a session
Args:
session_data (Dict): The Session object containing any new session_data
Returns:
@ -178,25 +456,15 @@ class Session:
"""
info = {"session_data": session_data}
url = f"{self.common_prefix}/users/{self.user_id}/sessions/{self.id}"
response = requests.put(url, json=info)
response = await self.client.put(url, json=info)
success = response.status_code < 400
self.session_data = session_data
return success
def delete(self):
"""Delete a session by marking it as inactive"""
async def close(self):
"""Closes a session by marking it as inactive"""
url = f"{self.common_prefix}/users/{self.user_id}/sessions/{self.id}"
response = requests.delete(url)
response = await self.client.delete(url)
response.raise_for_status()
self._is_active = False
class Message:
def __init__(self, session: Session, id: int, is_user: bool, content: str):
"""Constructor for Message"""
self.session = session
self.id = id
self.is_user = is_user
self.content = content
def __str__(self):
return f"Message(id={self.id}, is_user={self.is_user}, content={self.content})"

26
sdk/honcho/schemas.py Normal file
View File

@ -0,0 +1,26 @@
import uuid
import datetime
class Message:
def __init__(self, session_id: uuid.UUID, id: uuid.UUID, is_user: bool, content: str, created_at: datetime.datetime):
"""Constructor for Message"""
self.session_id = session_id
self.id = id
self.is_user = is_user
self.content = content
self.created_at = created_at
def __str__(self):
return f"Message(id={self.id}, is_user={self.is_user}, content={self.content})"
class Metamessage:
def __init__(self, id: uuid.UUID, message_id: uuid.UUID, metamessage_type: str, content: str, created_at: datetime.datetime):
"""Constructor for Metamessage"""
self.id = id
self.message_id = message_id
self.metamessage_type = metamessage_type
self.content = content
self.created_at = created_at
def __str__(self):
return f"Metamessage(id={self.id}, message_id={self.message_id}, metamessage_type={self.metamessage_type}, content={self.content})"

470
sdk/honcho/sync_client.py Normal file
View File

@ -0,0 +1,470 @@
import json
import uuid
from typing import Dict, Optional
import httpx
from .schemas import Message, Metamessage
class GetPage:
"""Base class for receiving Paginated API results"""
def __init__(self, response: Dict) -> None:
"""Constructor for Page with relevant information about the results and pages
Args:
response (Dict): Response from API with pagination information
"""
self.total = response["total"]
self.page = response["page"]
self.page_size = response["size"]
self.pages = response["pages"]
self.items =[]
def next(self):
"""Shortcut method to Get the next page of results"""
pass
class GetSessionPage(GetPage):
"""Paginated Results for Get Session Requests"""
def __init__(self, client, options: Dict, response: Dict):
"""Constructor for Page Result from Session Get Request
Args:
client (Client): Honcho Client
options (Dict): Options for the request used mainly for next() to filter queries. The two parameters available are user_id which is required and location_id which is optional
response (Dict): Response from API with pagination information
"""
super().__init__(response)
self.client = client
self.user_id = options["user_id"]
self.location_id = options["location_id"]
self.items = [
Session(
client=client,
id=session["id"],
user_id=session["user_id"],
location_id=session["location_id"],
is_active=session["is_active"],
session_data=session["session_data"],
)
for session in response["items"]
]
def next(self):
"""Get the next page of results
Returns:
GetSessionPage | None: Next Page of Results or None if there are no more sessions to retreive from a query
"""
if self.page >= self.pages:
return None
return self.client.get_sessions(self.user_id, self.location_id, self.page + 1, self.page_size)
class GetMessagePage(GetPage):
"""Paginated Results for Get Session Requests"""
def __init__(self, session, response: Dict):
"""Constructor for Page Result from Session Get Request
Args:
session (Session): Session the returned messages are associated with
response (Dict): Response from API with pagination information
"""
super().__init__(response)
self.session = session
self.items = [
Message(
session_id=session.id,
id=message["id"],
is_user=message["is_user"],
content=message["content"],
created_at=message["created_at"],
)
for message in response["items"]
]
def next(self):
"""Get the next page of results
Returns:
GetMessagePage | None: Next Page of Results or None if there are no more messages to retreive from a query
"""
if self.page >= self.pages:
return None
return self.session.get_messages((self.page + 1), self.page_size)
class GetMetamessagePage(GetPage):
def __init__(self, session, options: Dict, response: Dict) -> None:
"""Constructor for Page Result from Metamessage Get Request
Args:
session (Session): Session the returned messages are associated with
options (Dict): Options for the request used mainly for next() to filter queries. The two parameters available are message_id and metamessage_type which are both required
response (Dict): Response from API with pagination information
"""
super().__init__(response)
self.session = session
self.message_id = options["message_id"]
self.metamessage_type = options["metamessage_type"]
self.items = [
Metamessage(
id=metamessage["id"],
message_id=metamessage["message_id"],
metamessage_type=metamessage["metamessage_type"],
content=metamessage["content"],
created_at=metamessage["created_at"],
)
for metamessage in response["items"]
]
def next(self):
"""Get the next page of results
Returns:
GetMetamessagePage | None: Next Page of Results or None if there are no more metamessages to retreive from a query
"""
if self.page >= self.pages:
return None
return self.session.get_metamessages(metamessage_type=self.metamessage_type, message=self.message_id, page=(self.page + 1), page_size=self.page_size)
class Client:
"""Honcho API Client Object"""
def __init__(self, app_id: str, base_url: str = "https://demo.honcho.dev"):
"""Constructor for Client"""
self.base_url = base_url # Base URL for the instance of the Honcho API
self.app_id = app_id # Representing ID of the client application
self.client = httpx.Client()
@property
def common_prefix(self):
"""Shorcut for common API prefix. made a property to prevent tampering"""
return f"{self.base_url}/apps/{self.app_id}"
def get_session(self, user_id: str, session_id: uuid.UUID):
"""Get a specific session for a user by ID
Args:
user_id (str): The User ID representing the user, managed by the user
session_id (uuid.UUID): The ID of the Session to retrieve
Returns:
Session: The Session object of the requested Session
"""
url = f"{self.common_prefix}/users/{user_id}/sessions/{session_id}"
response = self.client.get(url)
response.raise_for_status()
data = response.json()
return Session(
client=self,
id=data["id"],
user_id=data["user_id"],
location_id=data["location_id"],
is_active=data["is_active"],
session_data=data["session_data"],
)
def get_sessions(self, user_id: str, location_id: Optional[str] = None, page: int = 1, page_size: int = 50):
"""Return sessions associated with a user paginated
Args:
user_id (str): The User ID representing the user, managed by the user
location_id (str, optional): Optional Location ID representing the location of a session
page (int, optional): The page of results to return
page_size (int, optional): The number of results to return
Returns:
GetSessionPage: Page or results for get_sessions query
"""
url = f"{self.common_prefix}/users/{user_id}/sessions?page={page}&size={page_size}" + (
f"&location_id={location_id}" if location_id else ""
)
response = self.client.get(url)
response.raise_for_status()
data = response.json()
options = {
"location_id": location_id,
"user_id": user_id
}
return GetSessionPage(self, options, data)
def get_sessions_generator(self, user_id: str, location_id: Optional[str] = None):
"""Shortcut Generator for get_sessions. Generator to iterate through all sessions for a user in an app
Args:
user_id (str): The User ID representing the user, managed by the user
location_id (str, optional): Optional Location ID representing the location of a session
Yields:
Session: The Session object of the requested Session
"""
page = 1
page_size = 50
get_session_response = self.get_sessions(user_id, location_id, page, page_size)
while True:
# get_session_response = self.get_sessions(user_id, location_id, page, page_size)
for session in get_session_response.items:
yield session
new_sessions = get_session_response.next()
if not new_sessions:
break
get_session_response = new_sessions
def create_session(
self, user_id: str, location_id: str = "default", session_data: Dict = {}
):
"""Create a session for a user
Args:
user_id (str): The User ID representing the user, managed by the user
location_id (str, optional): Optional Location ID representing the location of a session
session_data (Dict, optional): Optional session metadata
Returns:
Session: The Session object of the new Session
"""
data = {"location_id": location_id, "session_data": session_data}
url = f"{self.common_prefix}/users/{user_id}/sessions"
response = self.client.post(url, json=data)
response.raise_for_status()
data = response.json()
return Session(
self,
id=data["id"],
user_id=user_id,
location_id=location_id,
session_data=session_data,
is_active=data["is_active"],
)
class Session:
"""Represents a single session for a user in an app"""
def __init__(
self,
client: Client,
id: uuid.UUID,
user_id: str,
location_id: str,
session_data: dict | str,
is_active: bool,
):
"""Constructor for Session"""
self.base_url = client.base_url
self.client = client.client
self.app_id = client.app_id
self.id = id
self.user_id = user_id
self.location_id = location_id
self.session_data = (
session_data if isinstance(session_data, dict) else json.loads(session_data)
)
self._is_active = is_active
@property
def common_prefix(self):
"""Shortcut for common API prefix. made a property to prevent tampering"""
return f"{self.base_url}/apps/{self.app_id}"
def __str__(self):
"""String representation of Session"""
return f"Session(id={self.id}, app_id={self.app_id}, user_id={self.user_id}, location_id={self.location_id}, session_data={self.session_data}, is_active={self.is_active})"
@property
def is_active(self):
"""Returns whether the session is active - made property to prevent tampering"""
return self._is_active
def create_message(self, is_user: bool, content: str):
"""Adds a message to the session
Args:
is_user (bool): Whether the message is from the user
content (str): The content of the message
Returns:
Message: The Message object of the added message
"""
if not self.is_active:
raise Exception("Session is inactive")
data = {"is_user": is_user, "content": content}
url = f"{self.common_prefix}/users/{self.user_id}/sessions/{self.id}/messages"
response = self.client.post(url, json=data)
response.raise_for_status()
data = response.json()
return Message(session_id=self.id, id=data["id"], is_user=is_user, content=content, created_at=data["created_at"])
def get_message(self, message_id: uuid.UUID) -> Message:
"""Get a specific message for a session based on ID
Args:
message_id (uuid.UUID): The ID of the Message to retrieve
Returns:
Message: The Message object
"""
url = f"{self.common_prefix}/users/{self.user_id}/sessions/{self.id}/messages/{message_id}"
response = self.client.get(url)
response.raise_for_status()
data = response.json()
return Message(session_id=self.id, id=data["id"], is_user=data["is_user"], content=data["content"], created_at=data["created_at"])
def get_messages(self, page: int = 1, page_size: int = 50) -> GetMessagePage:
"""Get all messages for a session
Args:
page (int, optional): The page of results to return
page_size (int, optional): The number of results to return per page
Returns:
GetMessagePage: Page of Message objects
"""
url = f"{self.common_prefix}/users/{self.user_id}/sessions/{self.id}/messages?page={page}&size={page_size}"
response = self.client.get(url)
response.raise_for_status()
data = response.json()
return GetMessagePage(self, data)
def get_messages_generator(self):
"""Shortcut Generator for get_messages. Generator to iterate through all messages for a session in an app
Yields:
Message: The Message object of the next Message
"""
page = 1
page_size = 50
get_messages_page= self.get_messages(page, page_size)
while True:
# get_session_response = self.get_sessions(user_id, location_id, page, page_size)
for message in get_messages_page.items:
yield message
new_messages = get_messages_page.next()
if not new_messages:
break
get_messages_page = new_messages
def create_metamessage(self, message: Message, metamessage_type: str, content: str):
"""Adds a metamessage to a session and links it to a specific message
Args:
message (Message): A message to associate the metamessage with
metamessage_type (str): The type of the metamessage arbitrary itentifier
content (str): The content of the metamessage
Returns:
Metamessage: The Metamessage object of the added metamessage
"""
if not self.is_active:
raise Exception("Session is inactive")
data = {"metamessage_type": metamessage_type, "content": content, "message_id": message.id}
url = f"{self.common_prefix}/users/{self.user_id}/sessions/{self.id}/metamessages"
response = self.client.post(url, json=data)
response.raise_for_status()
data = response.json()
return Metamessage(id=data["id"], message_id=message.id, metamessage_type=metamessage_type, content=content, created_at=data["created_at"])
def get_metamessage(self, metamessage_id: uuid.UUID) -> Metamessage:
"""Get a specific metamessage
Args:
message_id (uuid.UUID): The ID of the Message to retrieve
Returns:
Message: The Message object
"""
url = f"{self.common_prefix}/users/{self.user_id}/sessions/{self.id}/metamessages/{metamessage_id}"
response = self.client.get(url)
response.raise_for_status()
data = response.json()
return Metamessage(id=data["id"], message_id=data["message_id"], metamessage_type=data["metamessage_type"], content=data["content"], created_at=data["created_at"])
def get_metamessages(self, metamessage_type: Optional[str] = None, message: Optional[Message] = None, page: int = 1, page_size: int = 50) -> GetMetamessagePage:
"""Get all messages for a session
Args:
user_id (str): The User ID representing the user, managed by the user
session_id (int): The ID of the Session to retrieve
Returns:
list[Dict]: List of Message objects
"""
url = f"{self.common_prefix}/users/{self.user_id}/sessions/{self.id}/metamessages?page={page}&size={page_size}"
if metamessage_type:
url += f"&metamessage_type={metamessage_type}"
if message:
url += f"&message_id={message.id}"
response = self.client.get(url)
response.raise_for_status()
data = response.json()
options = {
"metamessage_type": metamessage_type,
"message_id": message.id if message else None
}
return GetMetamessagePage(self, options, data)
def get_metamessages_generator(self, metamessage_type: Optional[str] = None, message: Optional[Message] = None):
"""Shortcut Generator for get_metamessages. Generator to iterate through all metamessages for a session in an app
Args:
metamessage_type (str, optional): Optional Metamessage type to filter by
message (Message, optional): Optional Message to filter by
Yields:
Metamessage: The next Metamessage object of the requested query
"""
page = 1
page_size = 50
get_metamessages_page = self.get_metamessages(metamessage_type=metamessage_type, message=message, page=page, page_size=page_size)
while True:
# get_session_response = self.get_sessions(user_id, location_id, page, page_size)
for metamessage in get_metamessages_page.items:
yield metamessage
new_messages = get_metamessages_page.next()
if not new_messages:
break
get_metamessages_page = new_messages
def update(self, session_data: Dict):
"""Update the session_data of a session
Args:
session_data (Dict): The Session object containing any new session_data
Returns:
boolean: Whether the session was successfully updated
"""
info = {"session_data": session_data}
url = f"{self.common_prefix}/users/{self.user_id}/sessions/{self.id}"
response = self.client.put(url, json=info)
success = response.status_code < 400
self.session_data = session_data
return success
def close(self):
"""Closes a session by marking it as inactive"""
url = f"{self.common_prefix}/users/{self.user_id}/sessions/{self.id}"
response = self.client.delete(url)
response.raise_for_status()
self._is_active = False

244
sdk/poetry.lock generated
View File

@ -1,9 +1,33 @@
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand.
[[package]]
name = "anyio"
version = "4.2.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"},
{file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"},
]
[package.dependencies]
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 (>=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"
version = "2023.11.17"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = ">=3.6"
files = [
@ -11,109 +35,11 @@ files = [
{file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"},
]
[[package]]
name = "charset-normalizer"
version = "3.3.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7.0"
files = [
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
{file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
{file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
{file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
{file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
{file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
{file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
{file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
{file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
{file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
{file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
{file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
]
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
@ -125,6 +51,7 @@ files = [
name = "exceptiongroup"
version = "1.2.0"
description = "Backport of PEP 654 (exception groups)"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -135,10 +62,70 @@ files = [
[package.extras]
test = ["pytest (>=6)"]
[[package]]
name = "h11"
version = "0.14.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
]
[[package]]
name = "httpcore"
version = "1.0.2"
description = "A minimal low-level HTTP client."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"},
{file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"},
]
[package.dependencies]
certifi = "*"
h11 = ">=0.13,<0.15"
[package.extras]
asyncio = ["anyio (>=4.0,<5.0)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (>=1.0.0,<2.0.0)"]
trio = ["trio (>=0.22.0,<0.23.0)"]
[[package]]
name = "httpx"
version = "0.26.0"
description = "The next generation HTTP client."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"},
{file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"},
]
[package.dependencies]
anyio = "*"
certifi = "*"
httpcore = ">=1.0.0,<2.0.0"
idna = "*"
sniffio = "*"
[package.extras]
brotli = ["brotli", "brotlicffi"]
cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (>=1.0.0,<2.0.0)"]
[[package]]
name = "idna"
version = "3.6"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=3.5"
files = [
@ -150,6 +137,7 @@ files = [
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -161,6 +149,7 @@ files = [
name = "packaging"
version = "23.2"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -172,6 +161,7 @@ files = [
name = "pluggy"
version = "1.3.0"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -187,6 +177,7 @@ testing = ["pytest", "pytest-benchmark"]
name = "pytest"
version = "7.4.4"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -206,30 +197,41 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "requests"
version = "2.31.0"
description = "Python HTTP for Humans."
name = "pytest-asyncio"
version = "0.23.4"
description = "Pytest support for asyncio"
category = "dev"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
{file = "pytest-asyncio-0.23.4.tar.gz", hash = "sha256:2143d9d9375bf372a73260e4114541485e84fca350b0b6b92674ca56ff5f7ea2"},
{file = "pytest_asyncio-0.23.4-py3-none-any.whl", hash = "sha256:b0079dfac14b60cd1ce4691fbfb1748fe939db7d0234b5aba97197d10fbe0fef"},
]
[package.dependencies]
certifi = ">=2017.4.17"
charset-normalizer = ">=2,<4"
idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<3"
pytest = ">=7.0.0,<8"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
[[package]]
name = "sniffio"
version = "1.3.0"
description = "Sniff out which async library your code is running under"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
{file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
]
[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -238,22 +240,18 @@ files = [
]
[[package]]
name = "urllib3"
version = "2.1.0"
description = "HTTP library with thread-safe connection pooling, file post, and more."
name = "typing-extensions"
version = "4.9.0"
description = "Backported and Experimental Type Hints for Python 3.8+"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"},
{file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"},
{file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"},
{file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"},
]
[package.extras]
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "0aa81b5a08a235ff5f275db8e6d0265d97fd1e350567862ee6b5afddaaf15620"
content-hash = "6ccea662fa5a5bae88618123d5d05e0d4955c234b7e1a688d2fae2f90cd9f7f8"

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "honcho-ai"
version = "0.0.1"
version = "0.0.2"
description = "Python Client SDK for Honcho"
authors = ["Plastic Labs <hello@plasticlabs.ai>"]
license = "AGPL-3.0"
@ -9,10 +9,11 @@ packages = [{include = "honcho"}]
[tool.poetry.dependencies]
python = "^3.10"
requests = "^2.31.0"
httpx = "^0.26.0"
[tool.poetry.group.test.dependencies]
pytest = "^7.4.4"
pytest-asyncio = "^0.23.4"
[build-system]
requires = ["poetry-core"]

274
sdk/tests/test_async.py Normal file
View File

@ -0,0 +1,274 @@
import pytest
from honcho import AsyncGetSessionPage, AsyncGetMessagePage, AsyncGetMetamessagePage, AsyncSession, Message, Metamessage
from honcho import AsyncClient as Honcho
from uuid import uuid1
@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)
assert retrieved_session.id == created_session.id
assert retrieved_session.is_active is True
assert retrieved_session.location_id == "default"
assert retrieved_session.session_data == {}
@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)
retrieved_sessions = response.items
assert len(retrieved_sessions) == 2
assert retrieved_sessions[0].id == created_session_1.id
assert retrieved_sessions[1].id == created_session_2.id
@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)
assert await created_session.update({"foo": "bar"})
retrieved_session = await client.get_session(user_id, created_session.id)
assert retrieved_session.session_data == {"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)
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)
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)
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)
response = await retrieved_session.get_messages()
messages = response.items
assert len(messages) == 2
user_message, ai_message = messages
assert user_message.content == "Hello"
assert user_message.is_user is True
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)
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)
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)
@pytest.mark.asyncio
async def test_paginated_sessions():
app_id = str(uuid1())
user_id = str(uuid1())
client = Honcho(app_id, "http://localhost:8000")
for i in range(10):
await client.create_session(user_id)
page = 1
page_size = 2
get_session_response = await client.get_sessions(user_id, page=page, page_size=page_size)
assert len(get_session_response.items) == page_size
assert get_session_response.pages == 5
new_session_response = await get_session_response.next()
assert new_session_response is not None
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)
assert len(final_page.items) == 2
next_page = await final_page.next()
assert next_page is None
@pytest.mark.asyncio
async def test_paginated_sessions_generator():
app_id = str(uuid1())
user_id = str(uuid1())
client = Honcho(app_id, "http://localhost:8000")
for i in range(3):
await client.create_session(user_id)
gen = client.get_sessions_generator(user_id)
# print(type(gen))
item = await gen.__anext__()
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")
for i in range(3):
await client.create_session(user_id)
page = 2
page_size = 50
get_session_response = await client.get_sessions(user_id, 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
@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)
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)
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)
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)
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()
item = await gen.__anext__()
assert isinstance(item, Message)
assert item.content == "Hello"
assert item.is_user is True
item2 = await gen.__anext__()
assert item2 is not None
assert item2.content == "Hi"
assert item2.is_user is False
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)
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}")
page_size = 7
page = await created_session.get_metamessages(page=1, page_size=page_size)
assert page is not None
assert isinstance(page, AsyncGetMetamessagePage)
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)
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)
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")
gen = created_session.get_metamessages_generator()
item = await gen.__anext__()
assert isinstance(item, Metamessage)
assert item.content == "Test 1"
assert item.metamessage_type == "thought"
item2 = await gen.__anext__()
assert item2 is not None
assert item2.content == "Test 2"
assert item2.metamessage_type == "thought"
with pytest.raises(StopAsyncIteration):
await gen.__anext__()

View File

@ -1,89 +0,0 @@
from honcho import Client
from uuid import uuid1
import pytest
def test_session_creation_retrieval():
app_id = str(uuid1())
client = Client(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)
assert retrieved_session.id == created_session.id
assert retrieved_session.is_active is True
assert retrieved_session.location_id == "default"
assert retrieved_session.session_data == {}
def test_session_multiple_retrieval():
app_id = str(uuid1())
client = Client(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)
retrieved_sessions = client.get_sessions(user_id)
assert len(retrieved_sessions) == 2
assert retrieved_sessions[0].id == created_session_1.id
assert retrieved_sessions[1].id == created_session_2.id
def test_session_update():
app_id = str(uuid1())
user_id = str(uuid1())
client = Client(app_id, "http://localhost:8000")
created_session = client.create_session(user_id)
assert created_session.update({"foo": "bar"})
retrieved_session = client.get_session(user_id, created_session.id)
assert retrieved_session.session_data == {"foo": "bar"}
def test_session_deletion():
app_id = str(uuid1())
user_id = str(uuid1())
client = Client(app_id, "http://localhost:8000")
created_session = client.create_session(user_id)
assert created_session.is_active is True
created_session.delete()
assert created_session.is_active is False
retrieved_session = client.get_session(user_id, created_session.id)
assert retrieved_session.is_active is False
assert retrieved_session.id == created_session.id
def test_messages():
app_id = str(uuid1())
user_id = str(uuid1())
client = Client(app_id, "http://localhost:8000")
created_session = client.create_session(user_id)
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)
messages = retrieved_session.get_messages()
assert len(messages) == 2
user_message, ai_message = messages
assert user_message.content == "Hello"
assert user_message.is_user is True
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 = Client(app_id, "http://localhost:8000")
created_session = client.create_session(user_id)
with pytest.raises(Exception):
for _ in range(10):
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 = Client(app_id_1, "http://localhost:8000")
client_2 = Client(app_id_2, "http://localhost:8000")
created_session = client_1.create_session(user_id)
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)

261
sdk/tests/test_sync.py Normal file
View File

@ -0,0 +1,261 @@
from honcho import GetSessionPage, GetMessagePage, GetMetamessagePage, Session, Message, Metamessage
from honcho import Client as Honcho
from uuid import uuid1
import pytest
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)
assert retrieved_session.id == created_session.id
assert retrieved_session.is_active is True
assert retrieved_session.location_id == "default"
assert retrieved_session.session_data == {}
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)
retrieved_sessions = response.items
assert len(retrieved_sessions) == 2
assert retrieved_sessions[0].id == created_session_1.id
assert retrieved_sessions[1].id == created_session_2.id
def test_session_update():
app_id = str(uuid1())
user_id = str(uuid1())
client = Honcho(app_id, "http://localhost:8000")
created_session = client.create_session(user_id)
assert created_session.update({"foo": "bar"})
retrieved_session = client.get_session(user_id, created_session.id)
assert retrieved_session.session_data == {"foo": "bar"}
def test_session_deletion():
app_id = str(uuid1())
user_id = str(uuid1())
client = Honcho(app_id, "http://localhost:8000")
created_session = client.create_session(user_id)
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)
assert retrieved_session.is_active is False
assert retrieved_session.id == created_session.id
def test_messages():
app_id = str(uuid1())
user_id = str(uuid1())
client = Honcho(app_id, "http://localhost:8000")
created_session = client.create_session(user_id)
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)
response = retrieved_session.get_messages()
messages = response.items
assert len(messages) == 2
user_message, ai_message = messages
assert user_message.content == "Hello"
assert user_message.is_user is True
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)
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)
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)
def test_paginated_sessions():
app_id = str(uuid1())
user_id = str(uuid1())
client = Honcho(app_id, "http://localhost:8000")
for i in range(10):
client.create_session(user_id)
page = 1
page_size = 2
get_session_response = client.get_sessions(user_id, page=page, page_size=page_size)
assert len(get_session_response.items) == page_size
assert get_session_response.pages == 5
new_session_response = get_session_response.next()
assert new_session_response is not None
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)
assert len(final_page.items) == 2
next_page = final_page.next()
assert next_page is None
def test_paginated_sessions_generator():
app_id = str(uuid1())
user_id = str(uuid1())
client = Honcho(app_id, "http://localhost:8000")
for i in range(3):
client.create_session(user_id)
gen = client.get_sessions_generator(user_id)
# print(type(gen))
item = next(gen)
assert item.user_id == user_id
assert isinstance(item, Session)
assert next(gen) is not None
assert next(gen) is not None
with pytest.raises(StopIteration):
next(gen)
def test_paginated_out_of_bounds():
app_id = str(uuid1())
user_id = str(uuid1())
client = Honcho(app_id, "http://localhost:8000")
for i in range(3):
client.create_session(user_id)
page = 2
page_size = 50
get_session_response = client.get_sessions(user_id, 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
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)
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)
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)
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)
created_session.create_message(is_user=True, content="Hello")
created_session.create_message(is_user=False, content="Hi")
gen = created_session.get_messages_generator()
item = next(gen)
assert isinstance(item, Message)
assert item.content == "Hello"
assert item.is_user is True
item2 = next(gen)
assert item2 is not None
assert item2.content == "Hi"
assert item2.is_user is False
with pytest.raises(StopIteration):
next(gen)
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)
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}")
page_size = 7
page = created_session.get_metamessages(page=1, page_size=page_size)
assert page is not None
assert isinstance(page, GetMetamessagePage)
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)
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)
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")
gen = created_session.get_metamessages_generator()
item = next(gen)
assert isinstance(item, Metamessage)
assert item.content == "Test 1"
assert item.metamessage_type == "thought"
item2 = next(gen)
assert item2 is not None
assert item2.content == "Test 2"
assert item2.metamessage_type == "thought"
with pytest.raises(StopIteration):
next(gen)