Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,6 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- **AI/ML Stack**: OpenAI API (GPT-4o-mini), Hugging Face embeddings, ChromaDB vector store, LangChain
- **Payment Processing**: Stripe integration for licensing
- **Database**: PostgreSQL with SQLAlchemy ORM
- **Third-party Libraries**:
- `vedana` (private Git dependency for core functionality)
- `btcopilot` (private Git dependency for AI copilot features)

### Data Architecture
- **Vector Database**: ChromaDB for embeddings stored in instance/vector_db/
Expand Down
11 changes: 11 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]

[dev-packages]

[requires]
python_version = "3.11"
104 changes: 85 additions & 19 deletions btcopilot/__init__.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,92 @@
EMBEDDINGS_MODEL = "sentence-transformers/all-MiniLM-L6-v2"

# # Best so far
# LLM_MODEL = "mistral"

LLM_MODEL = "gpt-4o-mini"

# LLM_MODEL = "mistral:7b-text-q8_0"

# LLM_MODEL = "tinyllama" # Had dirty answers

# LLM_MODEL = "deepseek-r1:14b"
# LLM_MODEL = "deepseek-r1:1.5b"
import sys
import os, logging
import hashlib, hmac, base64

IS_TEST = "pytest" in sys.modules

ACCESS_READ_ONLY = "ro"
ACCESS_READ_WRITE = "rw"

ROLE_ADMIN = "admin"
ROLE_SUBSCRIBER = "subscriber"
ROLE_AUDITOR = "auditor"

LICENSE_FREE = "com.vedanamedia.familydiagram.free"
LICENSE_BETA = "com.vedanamedia.familydiagram.beta"
LICENSE_ALPHA = "com.vedanamedia.familydiagram.alpha"
LICENSE_CLIENT = "com.vedanamedia.familydiagram.client"
LICENSE_CLIENT_ONCE = "com.vedanamedia.familydiagram.client.once"
LICENSE_PROFESSIONAL = "com.vedanamedia.familydiagram.professional"
LICENSE_PROFESSIONAL_MONTHLY = "com.vedanamedia.familydiagram.professional.monthly"
LICENSE_PROFESSIONAL_ANNUAL = "com.vedanamedia.familydiagram.professional.annual"
LICENSE_RESEARCHER = "com.vedanamedia.familydiagram.researcher"
LICENSE_RESEARCHER_MONTHLY = "com.vedanamedia.familydiagram.researcher.monthly"
LICENSE_RESEARCHER_ANNUAL = "com.vedanamedia.familydiagram.researcher.annual"
LICENSES = (
LICENSE_FREE,
LICENSE_BETA,
LICENSE_ALPHA,
LICENSE_CLIENT_ONCE,
LICENSE_PROFESSIONAL_MONTHLY,
LICENSE_PROFESSIONAL_ANNUAL,
LICENSE_RESEARCHER_MONTHLY,
LICENSE_RESEARCHER_ANNUAL,
)
LICENSES_FEATURES = (
LICENSE_FREE,
LICENSE_BETA,
LICENSE_ALPHA,
LICENSE_CLIENT,
LICENSE_PROFESSIONAL,
LICENSE_RESEARCHER_ANNUAL,
)


def any_license_match(x, y):
for _x in x:
for _y in y:
if _x.startswith(_y):
return True
return False


def licenses_features(licenses):
if not licenses:
return []
ret = set()
for x in licenses:
for feature in LICENSES_FEATURES:
if x["policy"]["code"].startswith(feature):
ret.add(feature)
ret = list(ret)
# Custom overrides here
if ret == [LICENSE_CLIENT]: # Client is an add-on to the free license
ret = [LICENSE_FREE, LICENSE_CLIENT]
return ret


SERVER_API_VERSION = "v1"


def httpAuthHeader(user, signature):
return "PKDiagram:%s:%s" % (user, signature)


def sign(secret, verb, content_md5, content_type, date, resource):
"""Just producce a signature."""
canonical = "\n".join((verb, content_md5, content_type, date, resource))
h = hmac.new(secret, canonical.encode("utf-8"), hashlib.sha1)
signature = base64.encodebytes(h.digest()).strip().decode("utf-8")
# Debug(verb, content_md5, content_type, date, resource, signature)
return signature


ANON_SECRET = b"17754e81c5cd0adb73bbeffb064ccbc6"
ANON_USER = "anonymous"


## https://www.redmadrobot.com/fyi/designing-mobile-app-architecture

import os, logging

# if os.getenv("FLASK_CONFIG") == "production":
# import ddtrace

Expand All @@ -26,10 +96,6 @@
_log = logging.getLogger(__name__)


from .app import create_app
from .auth import current_user, minimum_role, AnonUser


## Add Git SHA and Cache Headers

_git_sha = None
Expand Down
4 changes: 2 additions & 2 deletions btcopilot/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from flask import Flask, render_template, redirect, request, url_for
from werkzeug.exceptions import Unauthorized, HTTPException

import vedana
import btcopilot
from btcopilot.pro.copilot.engine import Engine
from btcopilot import auth, extensions, pro, personal, training

Expand Down Expand Up @@ -105,7 +105,7 @@ def _(e):
# return redirect(url_for("training.auth.login", next=request.url))
# else:
return (
render_template("errors/404.html", current_user=user, vedana=vedana),
render_template("errors/404.html", current_user=user, btcopilot=btcopilot),
404,
)

Expand Down
12 changes: 6 additions & 6 deletions btcopilot/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from flask import g, request, abort, session, redirect, url_for

import vedana
import btcopilot
from btcopilot.pro.models import User


Expand Down Expand Up @@ -117,7 +117,7 @@ def get_required_role() -> str:
# Get the current endpoint
endpoint = request.endpoint
if not endpoint:
return vedana.ROLE_SUBSCRIBER
return btcopilot.ROLE_SUBSCRIBER

# Get the view function
view_func = current_app.view_functions.get(endpoint)
Expand All @@ -142,7 +142,7 @@ def get_required_role() -> str:
if blueprint and hasattr(blueprint, "_minimum_role"):
return blueprint._minimum_role

return vedana.ROLE_SUBSCRIBER
return btcopilot.ROLE_SUBSCRIBER


def minimum_role(role):
Expand Down Expand Up @@ -187,9 +187,9 @@ def _authenticate_pro_personal_apps() -> User | None:
return abort(401)

authParts = auth_header.split(":")
if authParts[1] == vedana.ANON_USER:
if authParts[1] == btcopilot.ANON_USER:
user = AnonUser()
secret = vedana.ANON_SECRET
secret = btcopilot.ANON_SECRET
else:
username = authParts[1]
user = User.query.filter_by(username=username).first()
Expand All @@ -207,7 +207,7 @@ def _authenticate_pro_personal_apps() -> User | None:
resource = request.path + "?" + request.query_string.decode("utf-8")
else:
resource = request.path
ourSignature = vedana.sign(
ourSignature = btcopilot.sign(
secret, request.method, content_md5, content_type, date, resource
)
if ourSignature != theirSignature:
Expand Down
15 changes: 15 additions & 0 deletions btcopilot/extensions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@
)


EMBEDDINGS_MODEL = "sentence-transformers/all-MiniLM-L6-v2"

# # Best so far
# LLM_MODEL = "mistral"

LLM_MODEL = "gpt-4o-mini"

# LLM_MODEL = "mistral:7b-text-q8_0"

# LLM_MODEL = "tinyllama" # Had dirty answers

# LLM_MODEL = "deepseek-r1:14b"
# LLM_MODEL = "deepseek-r1:1.5b"


_log = logging.getLogger(__name__)


Expand Down
2 changes: 1 addition & 1 deletion btcopilot/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
def main_server():
from btcopilot import create_app
from btcopilot.app import create_app

app = create_app()
app.engine.llm()
Expand Down
1 change: 0 additions & 1 deletion btcopilot/personal/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from . import database
from . import chat
from .chat import ask, Response, ResponseDirection
from . import routes
Expand Down
20 changes: 10 additions & 10 deletions btcopilot/personal/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from btcopilot.async_utils import gather
from btcopilot.personal import pdp
from btcopilot.personal.models import Discussion, Statement
from btcopilot.personal.database import Database, PDP
from btcopilot.schema import DiagramData, PDP, asdict
from btcopilot.personal.prompts import (
ROLE_COACH_NOT_THERAPIST,
BOWEN_THEORY_COACHING_IN_A_NUTSHELL,
Expand Down Expand Up @@ -59,28 +59,28 @@ def ask(discussion: Discussion, user_statement: str) -> Response:

ai_log.info(f"User statement: {user_statement}")
if discussion.diagram:
database = discussion.diagram.get_database()
diagram_data = discussion.diagram.get_diagram_data()
else:
database = Database()
diagram_data = DiagramData()
results = gather(
pdp.update(discussion, database, user_statement),
pdp.update(discussion, diagram_data, user_statement),
detect_response_direction(user_statement, discussion),
)

(new_pdp, pdp_deltas), response_direction = results
ai_log.info(f"Response direction: {response_direction}")

# Write to disk
database.pdp = new_pdp
diagram_data.pdp = new_pdp
if discussion.diagram:
discussion.diagram.set_database(database)
discussion.diagram.set_diagram_data(diagram_data)

statement = Statement(
discussion_id=discussion.id,
text=user_statement,
speaker=discussion.chat_user_speaker,
order=discussion.next_order(),
pdp_deltas=pdp_deltas.model_dump() if pdp_deltas else None,
pdp_deltas=asdict(pdp_deltas) if pdp_deltas else None,
)
db.session.add(statement)

Expand Down Expand Up @@ -172,12 +172,12 @@ def ask(discussion: Discussion, user_statement: str) -> Response:
else:
raise ValueError(f"Unknown response direction: {response_direction}")

ai_response = _generate_response(discussion, database, meta_prompt)
ai_response = _generate_response(discussion, diagram_data, meta_prompt)
ai_log.info(f"AI response: {ai_response}")

response = Response(
statement=ai_response,
pdp=database.pdp,
pdp=diagram_data.pdp,
)
ai_statement = Statement(
discussion_id=discussion.id,
Expand All @@ -190,7 +190,7 @@ def ask(discussion: Discussion, user_statement: str) -> Response:


def _generate_response(
discussion: Discussion, database: Database, meta_prompt: str
discussion: Discussion, diagram_data: DiagramData, meta_prompt: str
) -> str:
"""
Generate a response from the AI based on the conversation history and the
Expand Down
Loading