Documentation Fixes and Discord Example

This commit is contained in:
Vineeth Voruganti 2024-01-18 13:03:33 -08:00
parent 9ea87cdb71
commit b5301e6f9d
23 changed files with 1425 additions and 2917 deletions

View File

@ -1,9 +1,9 @@
# Honcho
![Static Badge](https://img.shields.io/badge/Version-0.0.1-blue)
![Discord](https://img.shields.io/discord/1016845111637839922?style=flat&logo=discord&logoColor=23ffffff&label=Plastic%20Labs&labelColor=235865F2)
[![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)
![X (formerly Twitter) URL](https://img.shields.io/twitter/url?url=https%3A%2F%2Ftwitter.com%2Fplastic_labs)
[![X (formerly Twitter) URL](https://img.shields.io/twitter/url?url=https%3A%2F%2Ftwitter.com%2Fplastic_labs)](https://twitter.com/plastic_labs)
A User context management solution for building AI Agents and LLM powered
applications.
@ -39,26 +39,27 @@ specifying the appropriate environment variables.
1. Create a virtualenv and install the API's dependencies
```bash
cd honcho/api/
source $(poetry run poetry env info --path)/bin/activate
poetry install
cd honcho/api/ # change to the api directory
poetry shell # Activate virutal environment
poetry install # install dependencies
```
2. Edit the `.env.template` file to specify the type of database and
connection_uri. For testing sqlite is fine.
2. Copy the `.env.template` file and specify the type of database and
connection_uri. For testing sqlite is fine. The below example uses an
in-memory sqlite database.
> Honcho has been tested with Postgresql and SQLite
```env
DATABASE_TYPE=sqlite
CONNECTION_URI=sqlite:///./honcho.db
CONNECTION_URI=sqlite://
```
3. Run the API via uvicorn
```bash
cd honcho/api
source $(poetry run poetry env info --path)/bin/activate
cd honcho/api # change to the api directory
poetry shell # Activate virtual environment if not already enabled
python -m uvicorn src.main:app --reload
```
@ -78,16 +79,18 @@ docker run --env-file .env -p 8000:8000 honcho-api:latest
#### Deploy on Fly
The API can also be deployed on fly.io with the following commands:
The API can also be deployed on fly.io. Follow the [Fly.io
Docs](https://fly.io/docs/getting-started/) to setup your environment and the
`flyctl`.
Once `flyctl` is set up use the the following commands to launch the application:
```bash
cd honcho/api
fly launch
```
You can also add your secrets with the following shortcut command:
```bash
cat .env | fly secrets import
flyctl launch --no-deploy # Follow the prompts and edit as you see fit
cat .env | flyctl secrets import # Load in your secrets
flyctl deploy # Deploy with appropriate environment variables
```
### Client SDK

View File

@ -1,2 +1,2 @@
DATABASE_TYPE=sqlite
CONNECTION_URI=sqlite:///./sql_db.db
CONNECTION_URI=sqlite://

View File

@ -1,4 +1,5 @@
from fastapi import Depends, FastAPI, HTTPException
from typing import Optional
from sqlalchemy.orm import Session
import uvicorn
@ -22,7 +23,7 @@ def get_db():
########################################################
@app.get("/users/{user_id}/sessions", response_model=list[schemas.Session])
def get_sessions(user_id: str, location_id: str, db: Session = Depends(get_db)):
def get_sessions(user_id: str, location_id: Optional[str] = None, db: Session = Depends(get_db)):
"""Get All Sessions for a User
Args:

View File

@ -1,34 +0,0 @@
# https://pythonspeed.com/articles/base-image-python-docker-images/
# https://testdriven.io/blog/docker-best-practices/
FROM python:3.10-slim-bullseye
WORKDIR /app
# https://stackoverflow.com/questions/53835198/integrating-python-poetry-with-docker
ENV PYTHONFAULTHANDLER=1 \
PYTHONUNBUFFERED=1 \
PYTHONHASHSEED=random \
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100 \
POETRY_VERSION=1.4.1
RUN pip install "poetry==$POETRY_VERSION"
# Copy only requirements to cache them in docker layer
WORKDIR /app
COPY poetry.lock pyproject.toml /app/
# Project initialization:
RUN poetry config virtualenvs.create false \
&& poetry install --no-root --no-interaction --no-ansi --without dev
WORKDIR /app
RUN addgroup --system app && adduser --system --group app
USER app
COPY . .
# https://stackoverflow.com/questions/29663459/python-app-does-not-print-anything-when-running-detached-in-docker
CMD ["python", "-m", "uvicorn", "main:app", "--port", "80"]

View File

@ -1,24 +0,0 @@
from abc import ABC, abstractmethod
from typing import List, Dict
class Mediator(ABC):
@abstractmethod
def get_sessions(self, user_id: str, location_id: str) -> List[Dict] | None:
pass
@abstractmethod
def get_session(self, session_id: str) -> Dict | None:
pass
@abstractmethod
def add_session(self, user_id: str, location_id: str, metadata: Dict) -> Dict:
pass
@abstractmethod
def update_session(self, session_id: str, metadata: Dict) -> None:
pass
@abstractmethod
def delete_session(self, session_id: str) -> None:
pass

View File

@ -1,70 +0,0 @@
"""
Below is an implementation of a basic LRUcache that utilizes the built
in OrderedDict data structure.
"""
from collections import OrderedDict
from mediator import SupabaseMediator
import uuid
from typing import List, Dict
from langchain.schema import BaseMessage, Document
from pydantic import BaseModel
# import sentry_sdk
class Conversation:
"Wrapper Class for storing contexts between channels. Using an object to pass by reference avoid additional cache hits"
# @sentry_sdk.trace
def __init__(
self,
mediator: SupabaseMediator,
user_id: str,
session_id: str = str(uuid.uuid4()),
metadata: Dict = {},
):
self.mediator: SupabaseMediator = mediator
self.user_id = user_id
self.session_id = session_id
self.metadata: Dict = metadata
# @sentry_sdk.trace
def add_message(
self,
message_type: str,
message: BaseMessage,
) -> None:
self.mediator.add_message(self.session_id, message_type, message.content)
# @sentry_sdk.trace
def get_messages(self, message_type: str) -> List[BaseMessage]:
return self.mediator.get_messages(self.session_id, message_type)
# @sentry_sdk.trace
def delete(self) -> None:
self.mediator.delete_session(self.session_id)
# @sentry_sdk.trace
# def restart(self) -> None:
# self.delete()
# representation = self.mediator.add_session(
# user_id=self.user_id, location_id=self.location_id
# )
# self.session_id: str = representation["id"]
# self.metadata = representation["metadata"]
# vector DB fn
# @sentry_sdk.trace
def add_texts(self, texts: List[str]) -> None:
metadatas = [
{"session_id": self.session_id, "user_id": self.user_id}
for _ in range(len(texts))
]
self.mediator.vector_table.add_texts(texts, metadatas)
# vector DB fn
# @sentry_sdk.trace
def similarity_search(self, query: str, match_count: int = 5) -> List[Document]:
return self.mediator.vector_table.similarity_search(
query=query, k=match_count, filter={"user_id": self.user_id}
)

View File

@ -1,464 +0,0 @@
import os
from langchain.chat_models import ChatOpenAI, AzureChatOpenAI
from langchain.output_parsers.list import NumberedListOutputParser
from langchain.prompts import (
load_prompt,
ChatPromptTemplate,
SystemMessagePromptTemplate,
)
from langchain.schema import AIMessage, HumanMessage, BaseMessage
from dotenv import load_dotenv
from collections.abc import AsyncIterator
from cache import Conversation
from typing import List
from openai import BadRequestError
import sentry_sdk
load_dotenv()
SYSTEM_THOUGHT = load_prompt(
os.path.join(os.path.dirname(__file__), "prompts/thought.yaml")
)
SYSTEM_RESPONSE = load_prompt(
os.path.join(os.path.dirname(__file__), "prompts/response.yaml")
)
SYSTEM_THOUGHT_REVISION = load_prompt(
os.path.join(os.path.dirname(__file__), "prompts/thought_revision.yaml")
)
SYSTEM_USER_PREDICTION_THOUGHT = load_prompt(
os.path.join(os.path.dirname(__file__), "prompts/user_prediction_thought.yaml")
)
SYSTEM_USER_PREDICTION_THOUGHT_REVISION = load_prompt(
os.path.join(
os.path.dirname(__file__), "prompts/user_prediction_thought_revision.yaml"
)
)
SYSTEM_VOE_THOUGHT = load_prompt(
os.path.join(os.path.dirname(__file__), "prompts/voe_thought.yaml")
)
SYSTEM_VOE = load_prompt(os.path.join(os.path.dirname(__file__), "prompts/voe.yaml"))
SYSTEM_CHECK_VOE_LIST = load_prompt(
os.path.join(os.path.dirname(__file__), "prompts/check_voe_list.yaml")
)
class BloomChain:
"Wrapper class for encapsulating the multiple different chains used in reasoning for the tutor's thoughts"
llm: AzureChatOpenAI = AzureChatOpenAI(
deployment_name=os.environ["OPENAI_API_DEPLOYMENT_NAME"],
temperature=1.2,
model_kwargs={"top_p": 0.5},
)
parser_llm: AzureChatOpenAI = AzureChatOpenAI(
deployment_name=os.environ["OPENAI_API_DEPLOYMENT_NAME"]
)
system_voe_thought: SystemMessagePromptTemplate = SystemMessagePromptTemplate(
prompt=SYSTEM_VOE_THOUGHT
)
system_voe: SystemMessagePromptTemplate = SystemMessagePromptTemplate(
prompt=SYSTEM_VOE
)
system_check_voe_list: SystemMessagePromptTemplate = SystemMessagePromptTemplate(
prompt=SYSTEM_CHECK_VOE_LIST
)
system_thought: SystemMessagePromptTemplate = SystemMessagePromptTemplate(
prompt=SYSTEM_THOUGHT
)
system_thought_revision: SystemMessagePromptTemplate = SystemMessagePromptTemplate(
prompt=SYSTEM_THOUGHT_REVISION
)
system_response: SystemMessagePromptTemplate = SystemMessagePromptTemplate(
prompt=SYSTEM_RESPONSE
)
system_user_prediction_thought: SystemMessagePromptTemplate = (
SystemMessagePromptTemplate(prompt=SYSTEM_USER_PREDICTION_THOUGHT)
)
system_user_prediction_thought_revision: SystemMessagePromptTemplate = (
SystemMessagePromptTemplate(prompt=SYSTEM_USER_PREDICTION_THOUGHT_REVISION)
)
output_parser = NumberedListOutputParser()
def __init__(self) -> None:
pass
@classmethod
# @sentry_sdk.trace
def think(cls, cache: Conversation, input: str):
"""Generate Bloom's thought on the user."""
# load message history
thought_prompt = ChatPromptTemplate.from_messages(
[
cls.system_thought,
*cache.get_messages("thought"),
HumanMessage(content=input),
]
)
chain = thought_prompt | cls.llm
def save_new_messages(ai_response):
cache.add_message("thought", HumanMessage(content=input))
cache.add_message("thought", AIMessage(content=ai_response))
return Streamable(
chain.astream({}, {"tags": ["thought"], "metadata": {"conversation_id": cache.conversation_id, "user_id": cache.user_id}}),
save_new_messages
)
@classmethod
# @sentry_sdk.trace
def revise_thought(cls, cache: Conversation, input: str, thought: str):
"""Revise Bloom's thought about the user with retrieved personal data"""
# construct rag prompt, retrieve docs
query = f"input: {input}\n thought: {thought}"
docs = cache.similarity_search(query)
messages = ChatPromptTemplate.from_messages(
[
cls.system_thought_revision,
*cache.get_messages("thought_revision"),
HumanMessage(content=input),
]
)
chain = messages | cls.llm
def save_new_messages(ai_response):
cache.add_message("thought_revision", HumanMessage(content=input))
cache.add_message("thought_revision", AIMessage(content=ai_response))
return Streamable(
chain.astream({ "thought": thought, "retrieved_vectors": "\n".join(doc.page_content for doc in docs)}, {"tags": ["thought_revision"], "metadata": {"conversation_id": cache.conversation_id, "user_id": cache.user_id}}),
save_new_messages
)
@classmethod
# @sentry_sdk.trace
def respond(cls, cache: Conversation, thought: str, input: str):
"""Generate Bloom's response to the user."""
response_prompt = ChatPromptTemplate.from_messages(
[
cls.system_response,
*cache.get_messages("response"),
HumanMessage(content=input),
]
)
chain = response_prompt | cls.llm
def save_new_messages(ai_response):
cache.add_message("response", HumanMessage(content=input))
cache.add_message("response", AIMessage(content=ai_response))
return Streamable(
chain.astream({ "thought": thought }, {"tags": ["response"], "metadata": {"conversation_id": cache.conversation_id, "user_id": cache.user_id}}),
save_new_messages
)
@classmethod
# @sentry_sdk.trace
async def think_user_prediction(cls, cache: Conversation, input: str):
"""Generate a thought about what the user is going to say"""
messages = ChatPromptTemplate.from_messages(
[
cls.system_user_prediction_thought,
]
)
chain = messages | cls.llm
history = unpack_messages(cache.get_messages("response"))
user_prediction_thought = await chain.ainvoke(
{"history": history},
{
"tags": ["user_prediction_thought"],
"metadata": {
"conversation_id": cache.conversation_id,
"user_id": cache.user_id,
},
},
)
cache.add_message("user_prediction_thought", user_prediction_thought)
return user_prediction_thought.content
@classmethod
# @sentry_sdk.trace
async def revise_user_prediction_thought(
cls, cache: Conversation, user_prediction_thought: str, input: str
):
"""Revise the thought about what the user is going to say based on retrieval of VoE facts"""
messages = ChatPromptTemplate.from_messages(
[
cls.system_user_prediction_thought_revision,
]
)
chain = messages | cls.llm
# construct rag prompt, retrieve docs
query = f"input: {input}\n thought: {user_prediction_thought}"
docs = cache.similarity_search(query)
history = unpack_messages(cache.get_messages("response"))
user_prediction_thought_revision = await chain.ainvoke(
{
"history": history,
"user_prediction_thought": user_prediction_thought,
"retrieved_vectors": "\n".join(doc.page_content for doc in docs),
},
config={
"tags": ["user_prediction_thought_revision"],
"metadata": {
"conversation_id": cache.conversation_id,
"user_id": cache.user_id,
},
},
)
cache.add_message(
"user_prediction_thought_revision", user_prediction_thought_revision
)
return user_prediction_thought_revision.content
@classmethod
# @sentry_sdk.trace
async def think_violation_of_expectation(
cls, cache: Conversation, inp: str, user_prediction_thought_revision: str
) -> None:
"""Assess whether expectation was violated, derive and store facts"""
# format prompt
messages = ChatPromptTemplate.from_messages([cls.system_voe_thought])
chain = messages | cls.llm
voe_thought = await chain.ainvoke(
{
"user_prediction_thought_revision": user_prediction_thought_revision,
"actual": inp,
},
config={"tags": ["voe_thought"], "metadata": {"user_id": cache.user_id}},
)
cache.add_message("voe_thought", voe_thought)
return voe_thought.content
@classmethod
# @sentry_sdk.trace
async def violation_of_expectation(
cls,
cache: Conversation,
inp: str,
user_prediction_thought_revision: str,
voe_thought: str,
) -> None:
"""Assess whether expectation was violated, derive and store facts"""
# format prompt
messages = ChatPromptTemplate.from_messages([cls.system_voe])
chain = messages | cls.llm
voe = await chain.ainvoke(
{
"ai_message": cache.get_messages("response")[-1].content,
"user_prediction_thought_revision": user_prediction_thought_revision,
"actual": inp,
"voe_thought": voe_thought,
},
config={"tags": ["voe"], "metadata": {"user_id": cache.user_id}},
)
cache.add_message("voe", voe)
facts = cls.output_parser.parse(voe.content)
return facts
@classmethod
# @sentry_sdk.trace
async def check_voe_list(cls, cache: Conversation, facts: List[str]):
"""Filter the facts to just new ones"""
# create the message object from prompt template
messages = ChatPromptTemplate.from_messages([cls.system_check_voe_list])
chain = messages | cls.llm
# unpack the list of strings into one string for similarity search
# TODO: should we query 1 by 1 and append to an existing facts list?
query = " ".join(facts)
# query the vector store
existing_facts = cache.similarity_search(query, match_count=10)
filtered_facts = await chain.ainvoke(
{
"existing_facts": "\n".join(
fact.page_content for fact in existing_facts
),
"facts": "\n".join(fact for fact in facts),
},
config={"tags": ["check_voe_list"], "metadata": {"user_id": cache.user_id}},
)
data = cls.output_parser.parse(filtered_facts.content)
# if the check returned "None", write facts to cache
if not data:
cache.add_texts(facts)
else:
cache.add_texts(data)
@classmethod
# @sentry_sdk.trace
async def chat(cls, cache: Conversation, inp: str) -> tuple[str, str]:
# VoE has to happen first. If there's user prediction history, derive and store fact(s)
if cache.get_messages("user_prediction_thought_revision"):
user_prediction_thought_revision = cache.get_messages(
"user_prediction_thought_revision"
)[-1].content
voe_thought = await cls.think_violation_of_expectation(
cache, inp, user_prediction_thought_revision
)
voe_facts = await cls.violation_of_expectation(
cache, inp, user_prediction_thought_revision, voe_thought
)
if not voe_facts or voe_facts[0] == "None":
pass
else:
await cls.check_voe_list(cache, voe_facts)
thought_iterator = cls.think(cache, inp)
thought = await thought_iterator()
thought_revision_iterator = cls.revise_thought(cache, inp, thought)
thought_revision = await thought_revision_iterator()
response_iterator = cls.respond(cache, thought_revision, inp)
response = await response_iterator()
user_prediction_thought = await cls.think_user_prediction(cache, inp)
user_prediction_thought_revision = await cls.revise_user_prediction_thought(
cache, user_prediction_thought, inp
)
return thought, response
@classmethod
# @sentry_sdk.trace
async def stream(cls, cache: Conversation, inp: str):
# VoE has to happen first. If there's user prediction history, derive and store fact(s)
try:
if cache.get_messages("user_prediction_thought_revision"):
user_prediction_thought_revision = cache.get_messages(
"user_prediction_thought_revision"
)[-1].content
voe_thought = await cls.think_violation_of_expectation(
cache, inp, user_prediction_thought_revision
)
voe_facts = await cls.violation_of_expectation(
cache, inp, user_prediction_thought_revision, voe_thought
)
if not voe_facts or voe_facts[0] == "None":
pass
else:
await cls.check_voe_list(cache, voe_facts)
print("=========================================")
print("Finished Init")
print("=========================================")
thought_iterator = cls.think(cache, inp)
thought = ""
async for item in thought_iterator:
# escape ❀ if present
item = item.replace("", "🌸")
thought += item
yield item
yield ""
print("=========================================")
print("Finished Thought")
print("=========================================")
thought_revision_iterator = cls.revise_thought(cache, inp, thought)
thought_revision = await thought_revision_iterator()
response_iterator = cls.respond(cache, thought_revision, inp)
# response = ""
async for item in response_iterator:
# if "❀" in item:
item = item.replace("", "🌸")
# response += item
yield item
print("=========================================")
print("Finished Response")
print("=========================================")
user_prediction_thought = await cls.think_user_prediction(cache, inp)
user_prediction_thought_revision = await cls.revise_user_prediction_thought(
cache, user_prediction_thought, inp
)
print("=========================================")
print("Finished User Prediction")
print("=========================================")
finally:
yield ""
class Streamable:
"A async iterator wrapper for langchain streams that saves on completion via callback"
def __init__(self, iterator: AsyncIterator[BaseMessage], callback):
self.iterator = iterator
self.callback = callback
# self.content: List[Awaitable[BaseMessage]] = []
self.content = ""
def __aiter__(self):
return self
async def __anext__(self):
try:
data = await self.iterator.__anext__()
self.content += data.content
return data.content
except StopAsyncIteration as e:
self.callback(self.content)
raise StopAsyncIteration
except BadRequestError as e:
if e.code == "content_filter":
self.stream_error = True
self.message = "Sorry, your message was flagged as inappropriate. Please try again."
return self.message
except Exception as e:
raise e
async def __call__(self):
async for _ in self:
pass
return self.content
def unpack_messages(messages):
unpacked = ""
for message in messages:
if isinstance(message, HumanMessage):
unpacked += f"User: {message.content}\n"
elif isinstance(message, AIMessage):
unpacked += f"AI: {message.content}\n"
# Add more conditions here if you're using other message types
return unpacked

View File

@ -1,151 +0,0 @@
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse, JSONResponse
from pydantic import BaseModel
from typing import Optional, Dict
import asyncio
# Local
from chain import BloomChain
from mediator import SupabaseMediator
from cache import Conversation
import os
from dotenv import load_dotenv
import sentry_sdk
load_dotenv()
rate = 0.2 if os.getenv("SENTRY_ENVIRONMENT") == "production" else 1.0
sentry_sdk.init(
dsn=os.environ["SENTRY_DSN"], traces_sample_rate=rate, profiles_sample_rate=rate
)
app = FastAPI()
MEDIATOR = SupabaseMediator()
LOCK = asyncio.Lock()
### User Routes ###
class User(BaseModel):
user_id: str
metadata: Optional[Dict]
@app.get("/users")
async def get_users():
# TODO update to return a list of ID's
pass
@app.get("/users/{user_id}")
async def get_user(user_id: str):
# TODO: return user metadata
pass
### Session Meta Routes ###
class UnknownSession(BaseModel):
location_id: str = "default"
class SessionMeta(BaseModel):
location_id: str = "default"
metadata: Optional[Dict] = None
@app.get("/users/{user_id}/sessions")
async def get_sessions(user_id, inp: UnknownSession):
"""Return session ids and metadata associated with a user and location"""
print(user_id, inp)
# TODO update to return a list of ID's
async with LOCK:
data: str = MEDIATOR.get_sessions(user_id, inp.location_id)
print(data)
return JSONResponse(status_code=200, content=data)
@app.post("/users/{user_id}/sessions")
async def add_session(user_id, inp: SessionMeta):
async with LOCK:
data = MEDIATOR.add_session(user_id, inp.location_id, inp.metadata)
return JSONResponse(status_code=200, content=data)
@app.get("/users/{user_id}/sessions/{session_id}")
async def get_session(user_id, session_id):
"""Return session metadata"""
async with LOCK:
data: str = MEDIATOR.get_session(session_id)
return JSONResponse(status_code=200, content=data)
@app.put("/users/{user_id}/sessions/{session_id}")
async def update_session(user_id, session_id, inp: SessionMeta):
async with LOCK:
MEDIATOR.update_session(session_id, inp.metadata)
return JSONResponse(status_code=200, content={"message": "OK"})
@app.delete("/users/{user_id}/sessions/{session_id}")
async def delete_session(user_id, session_id):
"""Delete a specific session"""
async with LOCK:
MEDIATOR.delete_session(session_id)
return JSONResponse(status_code=200, content={"message": "OK"})
### Session Message Routes ###
class Message(BaseModel):
message: str
message_type: str
@app.get("/users/{user_id}/sessions/{session_id}/messages")
async def get_messages(user_id, session_id):
"""Return messages associated with a session"""
async with LOCK:
data: str = MEDIATOR.get_messages(session_id)
return JSONResponse(status_code=200, content=data)
@app.post("/users/{user_id}/sessions/{session_id}/messages")
async def add_message(user_id, session_id, inp: Message):
"""Add a message to a session"""
async with LOCK:
MEDIATOR.add_message(session_id, inp.message_type, inp.message)
return JSONResponse(status_code=200, content={"message": "OK"})
class ChatInput(BaseModel):
message: str
@app.post("/users/{user_id}/sessions/{session_id}/chat")
async def voe(user_id, session_id, inp: ChatInput):
async with LOCK:
session = Conversation(MEDIATOR, user_id, session_id)
if session is None:
raise HTTPException(status_code=404, detail="Item not found")
thought, response = await BloomChain.chat(session, inp.message)
return {"thought": thought, "response": response}
# @app.post("/stream")
# async def stream(inp: SessionInput):
# async with LOCK:
# session = Conversation(MEDIATOR, user_id=inp.user_id, session_id=inp.session_id)
# if session is None:
# raise HTTPException(status_code=404, detail="Item not found")
# return StreamingResponse(BloomChain.stream(session, inp.message))
### API Fundamentals
# @app.post('/tenant/new')
# @app.post('application/new')
# @app.get('/tenant')
## Honcho Utilities
# @app.get('/theoryofmind')
# @app.get('/voe')

View File

@ -1,158 +0,0 @@
# from langchain.memory import PostgresChatMessageHistory
from langchain.schema import Document
from langchain.schema.messages import BaseMessage, _message_to_dict, messages_from_dict
from langchain.vectorstores import SupabaseVectorStore
from langchain.embeddings.base import Embeddings
from langchain.embeddings.openai import OpenAIEmbeddings
import uuid
import sentry_sdk
import os
from dotenv import load_dotenv
# Supabase for Postgres Management
from supabase.client import create_client, Client
from typing import List, Dict
import json
from abcs import Mediator
load_dotenv()
class SupabaseMediator(Mediator):
@sentry_sdk.trace
def __init__(self):
self.supabase: Client = create_client(
os.environ["SUPABASE_URL"], os.environ["SUPABASE_KEY"]
)
self.memory_table = os.environ["MEMORY_TABLE"]
self.session_table = os.environ["SESSION_TABLE"]
self.match_function = os.environ["MATCH_FUNCTION"]
embeddings = OpenAIEmbeddings(
deployment=os.environ["OPENAI_API_EMBEDDING_NAME"],
model="text-embedding-ada-002",
openai_api_base=os.environ["OPENAI_API_BASE"],
openai_api_type=os.environ["OPENAI_API_TYPE"],
)
self.vector_table = SupabaseVectorStore(
embedding=embeddings,
client=self.supabase,
table_name=os.environ["VECTOR_TABLE"],
query_name=self.match_function,
)
# self.supabase.table(self.session_table).insert({"id": session_id, "user_id": user_id, "location_id": location_id}).execute()
@sentry_sdk.trace
def get_sessions(self, user_id: str, location_id: str | None):
try:
data = (
self.supabase.table(self.session_table)
.select("*")
.eq("user_id", user_id)
)
print(location_id)
data = (
data.eq("location_id", location_id) if location_id is not None else data
)
response = (
data.eq("isActive", True).order("created_at", desc=True).execute()
)
print("response", response)
if response is not None:
return response.data
return None
except Exception as e:
print("========================================")
print(e)
print("========================================")
return None
@sentry_sdk.trace
def get_session(self, session_id):
response = (
self.supabase.table(self.session_table)
.select("*")
.eq("id", session_id)
.eq("is_active", True)
.single()
.execute()
)
if response:
return response.data
return None
@sentry_sdk.trace
def add_session(self, user_id: str, location_id: str, metadata: Dict) -> Dict:
session_id = str(uuid.uuid4())
payload = {
"id": session_id,
"user_id": user_id,
"location_id": location_id,
"metadata": metadata,
}
representation = self.supabase.table(self.session_table).insert(payload, returning="representation").execute() # type: ignore
print("========================================")
print(representation)
print("========================================")
return representation.data[0]
@sentry_sdk.trace
def update_session(self, session_id: str, metadata: Dict) -> None:
cur = (
self.supabase.table(self.session_table)
.select("metadata")
.eq("id", session_id)
.single()
.execute()
)
if cur.data["metadata"] is not None:
new_metadata = cur.data["metadata"].copy()
new_metadata.update(metadata)
else:
new_metadata = metadata
self.supabase.table(self.session_table).update({"metadata": new_metadata}, returning="representation").eq("id", session_id).execute() # type: ignore
@sentry_sdk.trace
def delete_session(self, session_id: str) -> None:
self.supabase.table(self.session_table).update({"isActive": False}).eq(
"id", session_id
).execute()
# Session Helper Methods
@sentry_sdk.trace
def _cleanup_sessions(self, session_ids: List[str]) -> None:
for session_id in session_ids:
self.supabase.table(self.session_table).update({"isActive": False}).eq(
"id", session_id
).execute()
# Session Usage Read & Write
@sentry_sdk.trace
def get_messages(self, session_id: str, type: str | None = None) -> List[BaseMessage]: # type: ignore
response = (
self.supabase.table(self.memory_table)
.select("*")
.eq("session_id", session_id)
)
response = (
(response if type is None else response.eq("type", type))
.order("created_at")
.execute()
)
print(response)
return response.data
@sentry_sdk.trace
def add_message(self, session_id: str, message_type: str, content: str) -> None:
payload = {
"session_id": session_id,
"type": message_type,
"content": content,
}
self.supabase.table(self.memory_table).insert(payload).execute()

1843
archive/poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +0,0 @@
_type: prompt
input_variables:
["existing_facts", "facts"]
template: >
Your job is to compare the following two lists and keep only unique items:
Old: """
{existing_facts}
"""
New: """
{facts}
"""
Remove redundant information from the new list and output the remaining facts. Your response should be a numbered list with each fact on a new line, for example: `\n\n1. foo\n\n2. bar\n\n3. baz`. If there's nothing to remove (i.e. the statements are sufficiently different), print "None".

View File

@ -1,11 +0,0 @@
_type: prompt
input_variables:
["thought"]
template: >
You are Bloom, a subversive-minded learning companion with deep expertise in Socratic dialogue. Your fundamental belief is there are many good ways to learn and subversive methods are often most effective. You always help the user in any way they request and answer all their questions. Below is a theory-of-mind thought we generated about the user:
Thought: """
{thought}
"""
You must produce an appropriate response to the user input. Always end each response with an element that drives the conversation forward--e.g. a question, a critical comment, a contextualization, etc. If the user wants to end the conversation, always comply.

View File

@ -1,7 +0,0 @@
_type: prompt
input_variables:
[]
template: >
You are Bloom, a subversive-minded learning companion. Your job is to employ your theory of mind skills to predict the users mental state.
Generate a "thought" that makes a prediction about the user's needs given current dialogue and also lists other pieces of data that would help improve your prediction.

View File

@ -1,19 +0,0 @@
_type: prompt
input_variables:
["thought", "retrieved_vectors"]
template: >
You are tasked with revising theory of mind assessments about your user. Here is a thought generated by Bloom, the subversive learning companion:
Thought: """
{thought}
"""
Based on this thought, the following personal data has been retrieved:
Personal Data: """
{retrieved_vectors}
"""
Given the thought and personal data, revise the thought.
thought revision:

View File

@ -1,13 +0,0 @@
_type: prompt
input_variables:
["history"]
template: >
Generate a "thought" that makes a prediction about what the user will say based on the way the conversation has been going and also lists other pieces of data that would help improve your prediction.
History: ```
{history}
```
thought:

View File

@ -1,25 +0,0 @@
_type: prompt
input_variables:
["history", "user_prediction_thought", "retrieved_vectors"]
template: >
You are tasked with revising theory of mind "thoughts" about what the user is going to say. Here is the thought generated previously:
Thought: """
{user_prediction_thought}
"""
Based on this thought, the following personal data has been retrieved:
Personal Data: """
{retrieved_vectors}
"""
And here's the conversation history that was used to generate the original thought:
History: """
{history}
"""
Given the thought, conversation history, and personal data, revise the thought if you believe there are changes to be made.
thought revision:

View File

@ -1,23 +0,0 @@
_type: prompt
input_variables:
["ai_message", "user_prediction_thought_revision", "actual", "voe_thought"]
template: >
Below is the most recent AI message we sent to a user, a "thought" about what the user was going to say to that, what the user actually responded with, and a theory of mind prediction about the user's response. Derive a fact (or list of facts) about the user based on the difference between the original thought and their actual response plus the theory of mind prediction about that response.
Most recent AI message: """
{ai_message}
"""
Thought about what they were going to say: """
{user_prediction_thought_revision}
"""
Actual response: """
{actual}
"""
Theory of mind prediction about that response: """
{voe_thought}
"""
Provide the fact(s) solely in reference to the Actual response and theory of mind prediction about that response; i.e. do not derive a fact that negates the thought about what they were going to say. Do not speculate anything about the user. Each fact must contain enough specificity to stand alone. If there are many facts, list them out. Your response should be a numbered list with each item on a new line, for example: `\n\n1. foo\n\n2. bar\n\n3. baz`. If there's nothing to derive (i.e. the statements are sufficiently similar), print "None".

View File

@ -1,15 +0,0 @@
_type: prompt
input_variables:
["user_prediction_thought_revision", "actual"]
template: >
Below is a "thought" about what the user was going to say, and then what the user actually said. Generate a theory of mind prediction about the user based on the difference between the "thought" and actual response.
Thought: """
{user_prediction_thought_revision}
"""
Actual: """
{actual}
"""
Provide the theory of mind prediction solely in reference to the Actual statement, i.e. do not generate something that negates the thought. Do not speculate anything about the user.

View File

@ -1,24 +0,0 @@
[tool.poetry]
name = "honcho"
version = "0.1.0"
description = "Framework for Privacy Preserving Personalization of AI Agents"
authors = ["Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>"]
license = "AGPLv3"
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.9"
fastapi = "^0.103.1"
langchain = "~0.0.3"
supabase = "^1.0.4"
tiktoken = "^0.4.0"
openai = "^1.3.8"
pydantic = "^2.3.0"
python-dotenv = "^1.0.0"
uvicorn = "^0.23.2"
sentry-sdk = {extras = ["fastapi"], version = "^1.31.0"}
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

69
example/discord/main.py Normal file
View File

@ -0,0 +1,69 @@
import os
import discord
from dotenv import load_dotenv
load_dotenv()
from honcho import Client as HonchoClient
from honcho import LRUCache
intents = discord.Intents.default()
intents.messages = True
intents.message_content = True
CACHE = LRUCache(50) # Support 50 concurrent active conversations cached in memory
honcho = HonchoClient("http://localhost:8000")
bot = discord.Bot(intents=intents)
def get_or_create(user_id, location_id) -> int:
key = f"{user_id}+{location_id}"
session_id = CACHE.get(key)
if session_id is None:
session = honcho.create_session(user_id, location_id)
print(session)
session_id = session["id"]
CACHE.put(key, session_id)
return session_id
@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)
# key = f"{user_id}+{location_id}"
session_id = get_or_create(user_id, location_id)
inp = message.content
honcho.create_message_for_session(user_id, session_id, True, inp)
async with message.channel.typing():
await message.channel.send("Fake LLM Message")
honcho.create_message_for_session(user_id, session_id, False, "Fake LLM Message")
@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)
key = f"{user_id}+{location_id}"
session_id = CACHE.get(key)
if session_id is not None:
honcho.delete_session(user_id, session_id)
session = honcho.create_session(user_id, location_id)
session_id = session["id"]
CACHE.put(key, session_id)
msg = "Great! The conversation has been restarted. What would you like to talk about?"
honcho.create_message_for_session(user_id, session_id, False, msg)
await ctx.respond(msg)
bot.run(os.environ["BOT_TOKEN"])

1328
example/discord/poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,9 @@ packages = [{include = "honcho_discord_example"}]
[tool.poetry.dependencies]
python = "^3.11"
langchain = "^0.1.0"
honcho-ai = "^0.0.0.dev1"
py-cord = "^2.4.1"
python-dotenv = "^1.0.0"
[build-system]

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "honcho-ai"
version = "0.0.1"
version = "0.0.0.dev1"
description = "Python Client SDK for Honcho"
authors = ["Plastic Labs <hello@plasticlabs.ai>"]
license = "AGPL-3.0"
@ -8,7 +8,7 @@ readme = "README.md"
packages = [{include = "honcho"}]
[tool.poetry.dependencies]
python = "^3.11"
python = "^3.10"
requests = "^2.31.0"
[build-system]