Merge pull request #14 from lfnovo/model_mgmt

Model mgmt
This commit is contained in:
Luis Novo 2024-10-30 18:30:17 -03:00 committed by GitHub
commit 53883aab4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 1038 additions and 533 deletions

View file

@ -1,37 +1,22 @@
# DEFAULT MODEL_CONFIGURATIONS
DEFAULT_MODEL="openai/gpt-4o-mini"
SUMMARIZATION_MODEL="openai/gpt-4o-mini"
# OPENAI
# USE MODEL NAMES AS "openai/<modelname>"
# EXAMPLE - openai/gpt-4o-mini
OPENAI_API_KEY=
# ANTHROPIC
# USE MODEL NAMES AS "anthropic/<modelname>"
# EXAMPLE - anthropic/claude-3-5-sonnet-20240620
# ANTHROPIC_API_KEY=
# GEMINI
# USE MODEL NAMES AS "gemini/<modelname>"
# EXAMPLE - gemini/gemini-1.5-pro-002
# GEMINI_API_KEY=
# VERTEXAI
# USE MODEL NAMES AS "vertexai/<modelname>"
# EXAMPLE - vertexai/gemini-1.5-pro-002
# VERTEX_PROJECT=my-google-cloud-project-name
# GOOGLE_APPLICATION_CREDENTIALS=./google-credentials.json
# OLLAMA
# USE MODEL NAMES AS "ollama/<modelname>"
# EXAMPLE - ollama/gemma2
# OLLAMA_API_BASE="http://10.20.30.20:11434"
# OPEN ROUTER
# USE MODEL NAMES AS "openrouter/<modelname>"
# EXAMPLE - openrouter/nvidia/llama-3.1-nemotron-70b-instruct
# OPENROUTER_BASE_URL="https://openrouter.ai/api/v1"
# OPENROUTER_API_KEY=

View file

@ -51,3 +51,7 @@ docker-update-latest: docker-buildx-prepare
# Release with latest
docker-release-all: docker-release docker-update-latest
dev:
docker compose -f docker-compose.dev.yml up --build

View file

@ -29,7 +29,7 @@ services:
open_notebook:
image: lfnovo/open_notebook:latest
ports:
- "8502:8502"
- "8080:8502"
env_file:
- ./docker.env
depends_on:
@ -52,16 +52,29 @@ Go to the [Usage](docs/USAGE.md) page to learn how to use all features.
- **Multi-Notebook Support**: Organize your research across multiple notebooks effortlessly.
- **Broad Content Integration**: Works with links, PDFs, TXT files, PowerPoint presentations, YouTube videos, and pasted text (audio/video support coming soon).
- **Multi-model support**: Open AI, Anthropic, Gemini, Vertex AI, Open Router, Ollama.
- **Podcast Generator**: Automatically convert your notes into a podcast format.
- **Broad Content Integration**: Works with links, PDFs, EPUB, Office, TXT, Markdown files, YouTube videos, Audio files, Video files and pasted text.
- **AI-Powered Notes**: Write notes yourself or let the AI assist you in generating insights.
- **Recursive Summarization**: Tackle large content by recursively summarizing it.
- **Integrated Search Engines**: Built-in full-text and vector search for faster information retrieval.
- **Fine-Grained Context Management**: Choose exactly what to share with the AI to maintain control.
- **Podcast Generator**: Automatically convert your notes into a podcast format.
- **Multi-model support**: Open AI, Anthropic, Gemini, Vertex AI, Open Router, Ollama.
## 🚀 New Features
### v0.0.7 - Model Management 🗂️
- Manage your AI models and providers in a single interface
- Define default models for several tasks such as chat, transformation, embedding, etc
- Enabled support for Embedding models from Gemini, Vertex and Ollama
### v0.0.6 - ePub and Office files support 📄
You can now process ePub and Office files (Word, Excel, PowerPoint), extracting text and insights from them. Perfect for books, reports, presentations, and more.
### v0.0.5 - Audio and Video support 📽️
You can now process audio and video files, extracting transcripts and insights from them. Perfect for podcasts, interviews, lectures, and more.
### v0.0.4 - Podcasts 🎙️
You can now build amazing custom podcasts based on your own data. Customize your speakers, episode structure, cadence, voices, etc.

View file

@ -1,29 +1,43 @@
import streamlit as st
from open_notebook.exceptions import InvalidDatabaseSchema, NoSchemaFound
from open_notebook.repository import check_database_version, execute_migration
from open_notebook.database.migrate import MigrationManager
# from open_notebook.config import DEFAULT_MODELS
from open_notebook.domain.models import DefaultModels
from stream_app.utils import version_sidebar
try:
version_sidebar()
check_database_version()
default_models = DefaultModels.load()
version_sidebar()
mm = MigrationManager()
if mm.needs_migration:
st.warning("The Open Notebook database needs a migration to run properly.")
if st.button("Run Migration"):
mm.run_migration_up()
st.success("Migration successful")
st.rerun()
elif (
not default_models.default_chat_model
or not default_models.default_transformation_model
):
st.warning(
"You don't have default chat and transformation models selected. Please, select them on the settings page."
)
elif not default_models.default_embedding_model:
st.warning(
"You don't have a default embedding model selected. Vector search will not be possible and your assistant will be less able to answer your queries. Please, select one on the settings page."
)
elif not default_models.default_speech_to_text_model:
st.warning(
"You don't have a default speech to text model selected. Your assistant will not be able to transcribe audio. Please, select one on the settings page."
)
elif not default_models.default_text_to_speech_model:
st.warning(
"You don't have a default text to speech model selected. Your assistant will not be able to generate audio and podcasts. Please, select one on the settings page."
)
elif not default_models.large_context_model:
st.warning(
"You don't have a large context model selected. Your assistant will not be able to process large documents. Please, select one on the settings page."
)
else:
st.switch_page("pages/2_📒_Notebooks.py")
except NoSchemaFound as e:
st.warning(e)
if st.button("Create Schema.."):
try:
execute_migration("db_setup.surrealql")
st.success("Schema created successfully")
st.rerun()
except Exception as e:
st.error(e)
except InvalidDatabaseSchema as e:
st.warning(e)
if st.button("Execute Migration.."):
try:
execute_migration("0_0_1_to_0_0_2.surrealql")
st.success("Migration executed successfully")
st.rerun()
except Exception as e:
st.error(e)
st.stop()

View file

@ -1,82 +0,0 @@
DEFINE FIELD full_text ON TABLE source TYPE option<string>;
REMOVE TABLE IF EXISTS source_chunk;
REMOVE INDEX IF EXISTS idx_source_full ON TABLE source_chunk;
DEFINE FIELD IF NOT EXISTS archived ON TABLE notebook TYPE option<bool> DEFAULT False;
DEFINE INDEX idx_source_full ON TABLE source_chunk COLUMNS content SEARCH ANALYZER my_analyzer BM25 HIGHLIGHTS;
REMOVE FUNCTION IF EXISTS fn::text_search;
DEFINE FUNCTION IF NOT EXISTS fn::text_search($query_text: string, $match_count: int, $sources:bool, $show_notes:bool) {
let $source_title_search =
IF $sources {(
SELECT id as item_id, math::max(search::score(1)) AS relevance
FROM source
WHERE title @1@ $query_text
GROUP BY item_id)}
ELSE { [] };
let $source_embedding_search =
IF $sources {(
SELECT source as item_id, math::max(search::score(1)) AS relevance
FROM source_embedding
WHERE content @1@ $query_text
GROUP BY item_id)}
ELSE { [] };
let $source_full_search =
IF $sources {(
SELECT source as item_id, math::max(search::score(1)) AS relevance
FROM source
WHERE full_text @1@ $query_text
GROUP BY item_id)}
ELSE { [] };
let $source_insight_search =
IF $sources {(
SELECT source as item_id, math::max(search::score(1)) AS relevance
FROM source_insight
WHERE content @1@ $query_text
GROUP BY item_id)}
ELSE { [] };
let $note_title_search =
IF $show_notes {(
SELECT id as item_id, math::max(search::score(1)) AS relevance
FROM note
WHERE title @1@ $query_text
GROUP BY item_id)}
ELSE { [] };
let $note_content_search =
IF $show_notes {(
SELECT id as item_id, math::max(search::score(1)) AS relevance
FROM note
WHERE content @1@ $query_text
GROUP BY item_id)}
ELSE { [] };
let $source_chunk_results = array::union($source_embedding_search, $source_full_search);
let $source_asset_results = array::union($source_title_search, $source_insight_search);
let $source_results = array::union($source_chunk_results, $source_asset_results );
let $note_results = array::union($note_title_search, $note_content_search );
let $final_results = array::union($source_results, $note_results );
RETURN (SELECT item_id, math::max(relevance) as relevance from $final_results
group by item_id ORDER BY relevance DESC LIMIT $match_count);
};
DEFINE EVENT IF NOT EXISTS source_delete ON TABLE source WHEN ($after == NONE) THEN {
delete source_embedding where source == $before.id;
delete source_insight where source == $before.id;
};
DEFINE TABLE IF NOT EXISTS podcast_config SCHEMALESS;
UPDATE open_notebook:database_info SET
version= "0.0.2";

View file

@ -63,7 +63,6 @@ services:
- "8080:8502"
environment:
- OPENAI_API_KEY=API_KEY
- DEFAULT_MODEL=gpt-4o-mini
- SURREAL_ADDRESSsurrealdb
- SURREAL_PORT=8000
- SURREAL_USER=root
@ -105,7 +104,7 @@ or the shourcut
make run
```
## Setting up the providers
## Setting up the providers and models
Several new providers are supported now:
@ -121,30 +120,33 @@ All providers are installed out of the box. All you need to do is to setup the e
Please refer to the `.env.example` file for instructions on which ENV variables are necessary for each.
### Use provider-modelname convention
### Create models on the Settings page
You should prepend the provider name to the model_name when setting up your env variables, examples:
Go to the settings page and create your different models.
- openai/gpt-4o-mini
- anthropic/claude-3-5-sonnet-20240620
- ollama/gemma2
- openrouter/nvidia/llama-3.1-nemotron-70b-instruct
- vertexai/gemini-1.5-flash-001
- gemini/gemini-1.5-flash-001
| Model Type | Supported Providers |
|------------|-----------|
| Language | OpenAI, Anthropic, Open Router, LiteLLM, Vertex AI, Vertex AI, Anthropic, Gemini, Ollama |
| Embedding | OpenAI, Gemini, Vertex AI, Ollama |
| Speech to Text | OpenAI |
| Text to Speech | OpenAI, ElevenLabs |
__There will be a UI configuration for models in the coming days.__
## Setup 2 models for more flexibility
> 📝 **Notice:** For complete usage of all the features, you need to setup at least 4 models (one of each type).
There are 2 configurations for models at this point:
After setting up the models, head to the Model Defaults tab to define the default models. There are several defaults to setup.
```
DEFAULT_MODEL="openai/gpt-4o-mini"
SUMMARIZATION_MODEL="openrouter/nvidia/llama-3.1-nemotron-70b-instruct"
```
- **DEFAULT_MODEL** is used by the chat tool
- **SUMMARIZATION_MODEL (optional)** is used on the content summarization
| Model Default | Purpose |
|------------|-----------|
| Chat Model | Will be used on all chats |
| Transformation Model | Will be used for summaries, insights, etc |
| Large Context | For content higher then 110k tokens (use Gemini here) |
| Speech to Text | For transcribing text from your audio/video uploads |
| Text to Speech | For generating podcasts |
| Embedding | For creating vector representation of content |
All model types and defaults are required for now. If you are not sure which to pick, go with OpenAI, the only one that covers all possible model types.
The reason for opting for this route is because different LLMs, will behave better/worse depending on the type of request and type of tools offered. So it makes sense to build a more refined system to decide which model should process which task.

View file

@ -1,92 +1,78 @@
REMOVE table IF EXISTS source;
REMOVE table IF EXISTS reference;
REMOVE table IF EXISTS notebook;
REMOVE table IF EXISTS note;
REMOVE table IF EXISTS artifact;
REMOVE table IF EXISTS source_chunk;
REMOVE table IF EXISTS source_insight;
REMOVE ANALYZER IF EXISTS my_analyzer;
REMOVE FUNCTION IF EXISTS fn::text_search;
REMOVE INDEX IF EXISTS idx_source_full ON TABLE source_chunk;
REMOVE INDEX IF EXISTS idx_source_embed_chunk ON TABLE source_embedding;
REMOVE INDEX IF EXISTS idx_source_insight ON TABLE source_insight;
REMOVE INDEX IF EXISTS idx_note ON TABLE note;
REMOVE INDEX IF EXISTS idx_source_title ON TABLE source;
REMOVE INDEX IF EXISTS idx_note_title ON TABLE note;
DEFINE TABLE IF NOT EXISTS source SCHEMAFULL;
DEFINE FIELD asset
DEFINE FIELD IF NOT EXISTS
asset
ON TABLE source
FLEXIBLE TYPE option<object>;
DEFINE FIELD title ON TABLE source TYPE option<string>;
DEFINE FIELD full_text ON TABLE source TYPE option<string>;
DEFINE FIELD topics ON TABLE source TYPE option<array<string>>;
DEFINE FIELD IF NOT EXISTS title ON TABLE source TYPE option<string>;
DEFINE FIELD IF NOT EXISTS topics ON TABLE source TYPE option<array<string>>;
DEFINE FIELD IF NOT EXISTS full_text ON TABLE source TYPE option<string>;
DEFINE FIELD created ON source DEFAULT time::now() VALUE $before OR time::now();
DEFINE FIELD updated ON source DEFAULT time::now() VALUE time::now();
DEFINE FIELD IF NOT EXISTS created ON source DEFAULT time::now() VALUE $before OR time::now();
DEFINE FIELD IF NOT EXISTS updated ON source DEFAULT time::now() VALUE time::now();
DEFINE TABLE IF NOT EXISTS source_embedding SCHEMAFULL;
DEFINE FIELD source ON TABLE source_embedding TYPE record<source>;
DEFINE FIELD order ON TABLE source_embedding TYPE int;
DEFINE FIELD content ON TABLE source_embedding TYPE string;
DEFINE FIELD embedding ON TABLE source_embedding TYPE array<float>;
DEFINE FIELD IF NOT EXISTS source ON TABLE source_embedding TYPE record<source>;
DEFINE FIELD IF NOT EXISTS order ON TABLE source_embedding TYPE int;
DEFINE FIELD IF NOT EXISTS content ON TABLE source_embedding TYPE string;
DEFINE FIELD IF NOT EXISTS embedding ON TABLE source_embedding TYPE array<float>;
DEFINE TABLE IF NOT EXISTS source_insight SCHEMAFULL;
DEFINE FIELD source ON TABLE source_insight TYPE record<source>;
DEFINE FIELD insight_type ON TABLE source_insight TYPE string;
DEFINE FIELD content ON TABLE source_insight TYPE string;
DEFINE FIELD embedding ON TABLE source_insight TYPE array<float>;
DEFINE FIELD IF NOT EXISTS source ON TABLE source_insight TYPE record<source>;
DEFINE FIELD IF NOT EXISTS insight_type ON TABLE source_insight TYPE string;
DEFINE FIELD IF NOT EXISTS content ON TABLE source_insight TYPE string;
DEFINE FIELD IF NOT EXISTS embedding ON TABLE source_insight TYPE array<float>;
DEFINE EVENT source_delete ON TABLE source WHEN ($after == NONE) THEN {
DEFINE EVENT IF NOT EXISTS source_delete ON TABLE source WHEN ($after == NONE) THEN {
delete source_embedding where source == $before.id;
delete source_insight where source == $before.id;
};
DEFINE TABLE IF NOT EXISTS note SCHEMAFULL;
DEFINE FIELD title ON TABLE note TYPE option<string>;
DEFINE FIELD summary ON TABLE note TYPE option<string>;
DEFINE FIELD content ON TABLE note TYPE option<string>;
DEFINE FIELD embedding ON TABLE note TYPE array<float>;
DEFINE FIELD IF NOT EXISTS title ON TABLE note TYPE option<string>;
DEFINE FIELD IF NOT EXISTS summary ON TABLE note TYPE option<string>;
DEFINE FIELD IF NOT EXISTS content ON TABLE note TYPE option<string>;
DEFINE FIELD IF NOT EXISTS embedding ON TABLE note TYPE array<float>;
DEFINE FIELD created ON note DEFAULT time::now() VALUE $before OR time::now();
DEFINE FIELD updated ON note DEFAULT time::now() VALUE time::now();
DEFINE FIELD IF NOT EXISTS created ON note DEFAULT time::now() VALUE $before OR time::now();
DEFINE FIELD IF NOT EXISTS updated ON note DEFAULT time::now() VALUE time::now();
DEFINE TABLE IF NOT EXISTS notebook SCHEMAFULL;
DEFINE FIELD name ON TABLE notebook TYPE option<string>;
DEFINE FIELD description ON TABLE notebook TYPE option<string>;
DEFINE FIELD archived ON TABLE notebook TYPE option<bool> DEFAULT False;
DEFINE FIELD IF NOT EXISTS name ON TABLE notebook TYPE option<string>;
DEFINE FIELD IF NOT EXISTS description ON TABLE notebook TYPE option<string>;
DEFINE FIELD IF NOT EXISTS archived ON TABLE notebook TYPE option<bool> DEFAULT False;
DEFINE FIELD created ON notebook DEFAULT time::now() VALUE $before OR time::now();
DEFINE FIELD updated ON notebook DEFAULT time::now() VALUE time::now();
DEFINE FIELD IF NOT EXISTS created ON notebook DEFAULT time::now() VALUE $before OR time::now();
DEFINE FIELD IF NOT EXISTS updated ON notebook DEFAULT time::now() VALUE time::now();
DEFINE TABLE reference
DEFINE TABLE IF NOT EXISTS reference
TYPE RELATION
FROM source TO notebook;
DEFINE TABLE artifact
DEFINE TABLE IF NOT EXISTS artifact
TYPE RELATION
FROM note TO notebook;
-- entender o analyzer
DEFINE ANALYZER my_analyzer TOKENIZERS blank,class,camel,punct FILTERS snowball(english), lowercase;
DEFINE TABLE IF NOT EXISTS podcast_config SCHEMALESS;
DEFINE INDEX idx_source_title ON TABLE source COLUMNS title SEARCH ANALYZER my_analyzer BM25 HIGHLIGHTS;
DEFINE INDEX idx_source_full_text ON TABLE source COLUMNS full_text SEARCH ANALYZER my_analyzer BM25 HIGHLIGHTS;
DEFINE INDEX idx_source_embed_chunk ON TABLE source_embedding COLUMNS content SEARCH ANALYZER my_analyzer BM25 HIGHLIGHTS;
DEFINE INDEX idx_source_insight ON TABLE source_insight COLUMNS content SEARCH ANALYZER my_analyzer BM25 HIGHLIGHTS;
DEFINE INDEX idx_note ON TABLE note COLUMNS content SEARCH ANALYZER my_analyzer BM25 HIGHLIGHTS;
DEFINE INDEX idx_note_title ON TABLE note COLUMNS title SEARCH ANALYZER my_analyzer BM25 HIGHLIGHTS;
-- entender o analyzer
DEFINE ANALYZER IF NOT EXISTS my_analyzer TOKENIZERS blank,class,camel,punct FILTERS snowball(english), lowercase;
DEFINE INDEX IF NOT EXISTS idx_source_title ON TABLE source COLUMNS title SEARCH ANALYZER my_analyzer BM25 HIGHLIGHTS;
DEFINE INDEX IF NOT EXISTS idx_source_full_text ON TABLE source COLUMNS full_text SEARCH ANALYZER my_analyzer BM25 HIGHLIGHTS;
DEFINE INDEX IF NOT EXISTS idx_source_embed_chunk ON TABLE source_embedding COLUMNS content SEARCH ANALYZER my_analyzer BM25 HIGHLIGHTS;
DEFINE INDEX IF NOT EXISTS idx_source_insight ON TABLE source_insight COLUMNS content SEARCH ANALYZER my_analyzer BM25 HIGHLIGHTS;
DEFINE INDEX IF NOT EXISTS idx_note ON TABLE note COLUMNS content SEARCH ANALYZER my_analyzer BM25 HIGHLIGHTS;
DEFINE INDEX IF NOT EXISTS idx_note_title ON TABLE note COLUMNS title SEARCH ANALYZER my_analyzer BM25 HIGHLIGHTS;
DEFINE FUNCTION IF NOT EXISTS fn::text_search($query_text: string, $match_count: int, $sources:bool, $show_notes:bool) {
let $source_title_search =
IF $sources {(
SELECT id as item_id, math::max(search::score(1)) AS relevance
@ -150,8 +136,6 @@ DEFINE FUNCTION IF NOT EXISTS fn::text_search($query_text: string, $match_count:
};
REMOVE FUNCTION fn::vector_search;
DEFINE FUNCTION IF NOT EXISTS fn::vector_search($query: array<float>, $match_count: int, $sources:bool, $show_notes:bool) {
let $source_embedding_search =
@ -188,10 +172,7 @@ DEFINE FUNCTION IF NOT EXISTS fn::vector_search($query: array<float>, $match_cou
};
CREATE open_notebook:database_info SET
version= "0.0.2";
UPDATE open_notebook:database_info SET
version= "0.0.2";
IF array::len(select * from open_notebook:default_models) == 0 THEN
CREATE open_notebook:default_models SET
default_chat_model= ""
END;

View file

@ -0,0 +1,24 @@
REMOVE TABLE IF EXISTS source;
REMOVE TABLE IF EXISTS source_embedding;
REMOVE TABLE IF EXISTS source_insight;
REMOVE TABLE IF EXISTS note;
REMOVE TABLE IF EXISTS notebook;
REMOVE TABLE IF EXISTS reference;
REMOVE TABLE IF EXISTS artifact;
REMOVE TABLE IF EXISTS podcast_config;
REMOVE EVENT IF EXISTS source_delete ON TABLE source;
REMOVE ANALYZER IF EXISTS my_analyzer;
REMOVE INDEX IF EXISTS idx_source_title ON TABLE source;
REMOVE INDEX IF EXISTS idx_source_full_text ON TABLE source;
REMOVE INDEX IF EXISTS idx_source_embed_chunk ON TABLE source_embedding;
REMOVE INDEX IF EXISTS idx_source_insight ON TABLE source_insight;
REMOVE INDEX IF EXISTS idx_note ON TABLE note;
REMOVE INDEX IF EXISTS idx_note_title ON TABLE note;
REMOVE FUNCTION IF EXISTS fn::text_search;
REMOVE FUNCTION IF EXISTS fn::vector_search;
DELETE open_notebook:default_models;

View file

@ -3,7 +3,9 @@ import os
import yaml
from loguru import logger
# todo: enable config file overwrite with env vars
from open_notebook.domain.models import DefaultModels
from open_notebook.models import get_model
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
config_path = os.path.join(project_root, "open_notebook_config.yaml")
@ -32,3 +34,20 @@ os.makedirs(UPLOADS_FOLDER, exist_ok=True)
# PODCASTS FOLDER
PODCASTS_FOLDER = f"{DATA_FOLDER}/podcasts"
os.makedirs(PODCASTS_FOLDER, exist_ok=True)
DEFAULT_MODELS = DefaultModels.load()
if DEFAULT_MODELS.default_embedding_model:
EMBEDDING_MODEL = get_model(
DEFAULT_MODELS.default_embedding_model, model_type="embedding"
)
else:
EMBEDDING_MODEL = None
if DEFAULT_MODELS.default_speech_to_text_model:
SPEECH_TO_TEXT_MODEL = get_model(
DEFAULT_MODELS.default_speech_to_text_model, model_type="speech_to_text"
)
else:
SPEECH_TO_TEXT_MODEL = None

View file

@ -0,0 +1,56 @@
import os
from loguru import logger
from sblpy.connection import SurrealSyncConnection
from sblpy.migrations.db_processes import get_latest_version
from sblpy.migrations.migrations import Migration
from sblpy.migrations.runner import MigrationRunner
class MigrationManager:
def __init__(self):
self.connection = SurrealSyncConnection(
host=os.environ["SURREAL_ADDRESS"],
port=int(os.environ["SURREAL_PORT"]),
user=os.environ["SURREAL_USER"],
password=os.environ["SURREAL_PASS"],
namespace=os.environ["SURREAL_NAMESPACE"],
database=os.environ["SURREAL_DATABASE"],
encrypted=False, # Set to True if using SSL
)
self.up_migrations = [Migration.from_file("migrations/1.surrealql")]
self.down_migrations = [Migration.from_file("migrations/1_down.surrealql")]
self.runner = MigrationRunner(
up_migrations=self.up_migrations,
down_migrations=self.down_migrations,
connection=self.connection,
)
def get_current_version(self) -> int:
return get_latest_version(
self.connection.host,
self.connection.port,
self.connection.user,
self.connection.password,
self.connection.namespace,
self.connection.database,
)
@property
def needs_migration(self) -> bool:
current_version = self.get_current_version()
return current_version < len(self.up_migrations)
def run_migration_up(self):
current_version = self.get_current_version()
logger.debug(f"Current version before migration: {current_version}")
if self.needs_migration:
try:
self.runner.run()
new_version = self.get_current_version()
logger.debug(f"Migration successful. New version: {new_version}")
except Exception as e:
logger.error(f"Migration failed: {str(e)}")
else:
logger.debug("Database is already at the latest version")

View file

@ -5,10 +5,6 @@ from typing import Any, Dict, Optional
from loguru import logger
from sblpy.connection import SurrealSyncConnection
from open_notebook.exceptions import InvalidDatabaseSchema, NoSchemaFound
EXPECTED_VERSION = "0.0.2"
@contextmanager
def db_connection():
@ -34,30 +30,11 @@ def repo_query(query_str: str, vars: Optional[Dict[str, Any]] = None):
result = connection.query(query_str, vars)
return result
except Exception as e:
# logger.debug(f"Query: {query_str}, Variables: {vars}")
logger.critical(f"Query: {query_str}, Variables: {vars}")
logger.exception(e)
raise
def check_database_version():
try:
result = repo_query("SELECT * FROM open_notebook:database_info;")
if not result:
raise NoSchemaFound("Database schema not found")
version = result[0]["version"]
logger.info(f"Connected to SurrealDB, using schema version {version}")
if version != EXPECTED_VERSION:
raise InvalidDatabaseSchema(
f"Version mismatch. Expected {EXPECTED_VERSION}, got {version}"
)
except Exception as e:
logger.error(e)
raise e
def repo_create(table: str, data: Dict[str, Any]):
query = f"CREATE {table} CONTENT {data};"
# vars = {"table": table, "data": data}
@ -89,10 +66,3 @@ def repo_relate(source: str, relationship: str, target: str):
result = repo_query(query)
logger.debug(f"RELATE query result: {result}")
return result
def execute_migration(script: str):
with open(f"database/{script}", "r") as file:
content = file.read()
return repo_query(content)

View file

View file

@ -0,0 +1,147 @@
from datetime import datetime
from typing import Any, ClassVar, Dict, List, Optional, Type, TypeVar
from loguru import logger
from pydantic import BaseModel, ValidationError, field_validator
from open_notebook.database.repository import (
repo_create,
repo_delete,
repo_query,
repo_relate,
repo_update,
)
from open_notebook.exceptions import (
DatabaseOperationError,
InvalidInputError,
NotFoundError,
)
T = TypeVar("T", bound="ObjectModel")
class ObjectModel(BaseModel):
id: Optional[str] = None
table_name: ClassVar[str] = ""
created: Optional[datetime] = None
updated: Optional[datetime] = None
@classmethod
def get_all(cls: Type[T], order_by=None) -> List[T]:
try:
if order_by:
order = f" ORDER BY {order_by}"
else:
order = ""
result = repo_query(f"SELECT * FROM {cls.table_name} {order}")
objects = []
for obj in result:
try:
objects.append(cls(**obj))
except Exception as e:
logger.critical(f"Error creating object: {str(e)}")
return objects
except Exception as e:
logger.error(f"Error fetching all {cls.table_name}: {str(e)}")
logger.exception(e)
raise DatabaseOperationError(e)
@classmethod
def get(cls: Type[T], id: str) -> T:
if not id:
raise InvalidInputError("ID cannot be empty")
try:
result = repo_query(f"SELECT * FROM {id}")
if result:
return cls(**result[0])
return None
except Exception as e:
logger.error(f"Error fetching {cls.table_name} with id {id}: {str(e)}")
logger.exception(e)
raise NotFoundError(f"{cls.table_name} with id {id} not found")
def needs_embedding(self) -> bool:
return False
def get_embedding_content(self) -> Optional[str]:
return None
def save(self) -> None:
from open_notebook.config import EMBEDDING_MODEL
try:
logger.debug(f"Validating {self.__class__.__name__}")
self.model_validate(self.model_dump(), strict=True)
data = self._prepare_save_data()
data["updated"] = datetime.now().isoformat()
if self.needs_embedding():
embedding_content = self.get_embedding_content()
if embedding_content:
data["embedding"] = EMBEDDING_MODEL.embed(embedding_content)
if self.id is None:
data["created"] = datetime.now().isoformat()
logger.debug("Creating new record")
repo_result = repo_create(self.__class__.table_name, data)
else:
logger.debug(f"Updating record with id {self.id}")
repo_result = repo_update(self.id, data)
# Update the current instance with the result
for key, value in repo_result[0].items():
if hasattr(self, key):
if isinstance(getattr(self, key), BaseModel):
setattr(self, key, type(getattr(self, key))(**value))
else:
setattr(self, key, value)
except ValidationError as e:
logger.error(f"Validation failed: {e}")
raise
except Exception as e:
logger.error(f"Error saving record: {e}")
raise
except Exception as e:
logger.error(f"Error saving {self.__class__.table_name}: {str(e)}")
logger.exception(e)
raise DatabaseOperationError(e)
def _prepare_save_data(self) -> Dict[str, Any]:
data = self.model_dump()
# del data["created"]
# del data["updated"]
return {key: value for key, value in data.items() if value is not None}
def delete(self) -> bool:
if self.id is None:
raise InvalidInputError("Cannot delete object without an ID")
try:
logger.debug(f"Deleting record with id {self.id}")
return repo_delete(self.id)
except Exception as e:
logger.error(
f"Error deleting {self.__class__.table_name} with id {self.id}: {str(e)}"
)
raise DatabaseOperationError(
f"Failed to delete {self.__class__.table_name}"
)
def relate(self, relationship: str, target_id: str) -> Any:
if not relationship or not target_id or not self.id:
raise InvalidInputError("Relationship and target ID must be provided")
try:
return repo_relate(self.id, relationship, target_id)
except Exception as e:
logger.error(f"Error creating relationship: {str(e)}")
logger.exception(e)
raise DatabaseOperationError(e)
@field_validator("created", "updated", mode="before")
@classmethod
def parse_datetime(cls, value):
if isinstance(value, str):
return datetime.fromisoformat(value.replace("Z", "+00:00"))
return value

View file

@ -0,0 +1,46 @@
from typing import ClassVar, Optional
from pydantic import BaseModel
from open_notebook.database.repository import (
repo_query,
repo_update,
)
from open_notebook.domain.base import ObjectModel
class Model(ObjectModel):
table_name: ClassVar[str] = "model"
name: str
provider: str
type: str
@classmethod
def get_models_by_type(cls, model_type):
models = repo_query(
"SELECT * FROM model WHERE type=$model_type;", {"model_type": model_type}
)
return [Model(**model) for model in models]
class DefaultModels(BaseModel):
default_chat_model: Optional[str] = None
default_transformation_model: Optional[str] = None
large_context_model: Optional[str] = None
default_text_to_speech_model: Optional[str] = None
default_speech_to_text_model: Optional[str] = None
# default_vision_model: Optional[str] = None
default_embedding_model: Optional[str] = None
@classmethod
def load(self):
result = repo_query("SELECT * FROM open_notebook:default_models;")
if result:
result = result[0]
dm = DefaultModels(**result)
return dm
return DefaultModels()
@classmethod
def update(self, data):
repo_update("open_notebook:default_models", data)

View file

@ -1,153 +1,23 @@
import os
from datetime import datetime
from typing import Any, ClassVar, Dict, List, Literal, Optional, Type, TypeVar
from typing import Any, ClassVar, Dict, List, Literal, Optional
from langchain_core.runnables.config import RunnableConfig
from loguru import logger
from pydantic import BaseModel, Field, ValidationError, field_validator
from pydantic import BaseModel, Field, field_validator
from open_notebook.config import EMBEDDING_MODEL
from open_notebook.database.repository import (
repo_create,
repo_query,
)
from open_notebook.domain.base import ObjectModel
from open_notebook.exceptions import (
DatabaseOperationError,
InvalidInputError,
NotFoundError,
)
from open_notebook.graphs.multipattern import graph as pattern_graph
from open_notebook.graphs.recursive_toc import graph as toc_graph
from open_notebook.repository import (
repo_create,
repo_delete,
repo_query,
repo_relate,
repo_update,
)
from open_notebook.utils import get_embedding, split_text, surreal_clean
T = TypeVar("T", bound="ObjectModel")
class ObjectModel(BaseModel):
id: Optional[str] = None
table_name: ClassVar[str] = ""
created: Optional[datetime] = None
updated: Optional[datetime] = None
@classmethod
def get_all(cls: Type[T], order_by=None) -> List[T]:
try:
if order_by:
order = f" ORDER BY {order_by}"
else:
order = ""
result = repo_query(f"SELECT * FROM {cls.table_name} {order}")
objects = []
for obj in result:
try:
objects.append(cls(**obj))
except Exception as e:
logger.critical(f"Error creating object: {str(e)}")
return objects
except Exception as e:
logger.error(f"Error fetching all {cls.table_name}: {str(e)}")
logger.exception(e)
raise DatabaseOperationError(e)
@classmethod
def get(cls: Type[T], id: str) -> Optional[T]:
if not id:
raise InvalidInputError("ID cannot be empty")
try:
result = repo_query(f"SELECT * FROM {id}")
if result:
return cls(**result[0])
return None
except Exception as e:
logger.error(f"Error fetching {cls.table_name} with id {id}: {str(e)}")
logger.exception(e)
raise NotFoundError(f"{cls.table_name} with id {id} not found")
def needs_embedding(self) -> bool:
return False
def get_embedding_content(self) -> Optional[str]:
return None
def save(self) -> None:
try:
logger.debug(f"Validating {self.__class__.__name__}")
self.model_validate(self.model_dump(), strict=True)
data = self._prepare_save_data()
data["updated"] = datetime.now().isoformat()
if self.needs_embedding():
embedding_content = self.get_embedding_content()
if embedding_content:
data["embedding"] = get_embedding(embedding_content)
if self.id is None:
data["created"] = datetime.now().isoformat()
logger.debug("Creating new record")
repo_result = repo_create(self.__class__.table_name, data)
else:
logger.debug(f"Updating record with id {self.id}")
repo_result = repo_update(self.id, data)
# Update the current instance with the result
for key, value in repo_result[0].items():
if hasattr(self, key):
if isinstance(getattr(self, key), BaseModel):
setattr(self, key, type(getattr(self, key))(**value))
else:
setattr(self, key, value)
except ValidationError as e:
logger.error(f"Validation failed: {e}")
raise
except Exception as e:
logger.error(f"Error saving record: {e}")
raise
except Exception as e:
logger.error(f"Error saving {self.__class__.table_name}: {str(e)}")
logger.exception(e)
raise DatabaseOperationError(e)
def _prepare_save_data(self) -> Dict[str, Any]:
data = self.model_dump()
# del data["created"]
# del data["updated"]
return {key: value for key, value in data.items() if value is not None}
def delete(self) -> bool:
if self.id is None:
raise InvalidInputError("Cannot delete object without an ID")
try:
logger.debug(f"Deleting record with id {self.id}")
return repo_delete(self.id)
except Exception as e:
logger.error(
f"Error deleting {self.__class__.table_name} with id {self.id}: {str(e)}"
)
raise DatabaseOperationError(
f"Failed to delete {self.__class__.table_name}"
)
def relate(self, relationship: str, target_id: str) -> Any:
if not relationship or not target_id or not self.id:
raise InvalidInputError("Relationship and target ID must be provided")
try:
return repo_relate(self.id, relationship, target_id)
except Exception as e:
logger.error(f"Error creating relationship: {str(e)}")
logger.exception(e)
raise DatabaseOperationError(e)
@field_validator("created", "updated", mode="before")
@classmethod
def parse_datetime(cls, value):
if isinstance(value, str):
return datetime.fromisoformat(value.replace("Z", "+00:00"))
return value
from open_notebook.utils import split_text, surreal_clean
class Notebook(ObjectModel):
@ -288,7 +158,7 @@ class Source(ObjectModel):
"source": {self.id},
"order": {i},
"content": $content,
"embedding": {get_embedding(chunk)},
"embedding": {EMBEDDING_MODEL.embed(chunk)},
}};""",
{"content": surreal_clean(chunk)},
)
@ -322,7 +192,7 @@ class Source(ObjectModel):
if not insight_type or not content:
raise InvalidInputError("Insight type and content must be provided")
try:
embedding = get_embedding(content)
embedding = EMBEDDING_MODEL.embed(content)
return repo_query(
f"""
CREATE source_insight CONTENT {{
@ -396,9 +266,7 @@ class Note(ObjectModel):
return self.content
def text_search(
keyword: str, results: int, source: bool = True, note: bool = True
) -> List[Dict[str, Any]]:
def text_search(keyword: str, results: int, source: bool = True, note: bool = True):
if not keyword:
raise InvalidInputError("Search keyword cannot be empty")
try:
@ -415,9 +283,7 @@ def text_search(
raise DatabaseOperationError("Failed to perform text search")
def vector_search(
keyword: str, results: int, source: bool = True, note: bool = True
) -> List[Dict[str, Any]]:
def vector_search(keyword: str, results: int, source: bool = True, note: bool = True):
if not keyword:
raise InvalidInputError("Search keyword cannot be empty")
try:

View file

@ -16,12 +16,6 @@ class UnsupportedTypeException(OpenNotebookError):
pass
class NoSchemaFound(OpenNotebookError):
"""Raised when a database schema is not found."""
pass
class InvalidInputError(OpenNotebookError):
"""Raised when invalid input is provided."""
@ -70,12 +64,6 @@ class NetworkError(OpenNotebookError):
pass
class InvalidDatabaseSchema(OpenNotebookError):
"""Raised when the database is not under the expected schema."""
pass
class NoTranscriptFound(OpenNotebookError):
"""Raised when no transcript is found for a video."""

View file

@ -9,8 +9,8 @@ from langgraph.graph import END, START, StateGraph
from langgraph.graph.message import add_messages
from typing_extensions import TypedDict
from open_notebook.config import LANGGRAPH_CHECKPOINT_FILE
from open_notebook.domain import Notebook
from open_notebook.config import DEFAULT_MODELS, LANGGRAPH_CHECKPOINT_FILE
from open_notebook.domain.notebook import Notebook
from open_notebook.graphs.utils import run_pattern
@ -22,7 +22,9 @@ class ThreadState(TypedDict):
def call_model_with_messages(state: ThreadState, config: RunnableConfig) -> dict:
model_name = config.get("configurable", {}).get("model_name", None)
model_name = config.get("configurable", {}).get(
"model_name", DEFAULT_MODELS.default_chat_model
)
ai_message = run_pattern(
"chat",
model_name,

View file

@ -4,9 +4,9 @@ from math import ceil
from loguru import logger
from pydub import AudioSegment
from open_notebook.config import SPEECH_TO_TEXT_MODEL
from open_notebook.graphs.content_processing.state import SourceState
# todo: add a speechtotext model to the config
# future: parallelize the transcription process
@ -73,9 +73,6 @@ def split_audio(input_file, segment_length_minutes=15, output_prefix=None):
def extract_audio(data: SourceState):
input_audio_path = data.get("file_path")
from openai import OpenAI
client = OpenAI()
audio_files = []
try:
@ -83,11 +80,7 @@ def extract_audio(data: SourceState):
transcriptions = []
for audio_file in audio_files:
with open(audio_file, "rb") as audio:
transcription = client.audio.transcriptions.create(
model="whisper-1", file=audio
)
transcriptions.append(transcription.text)
transcriptions.append(SPEECH_TO_TEXT_MODEL.transcribe(audio_file))
return {"content": " ".join(transcriptions)}

View file

@ -6,7 +6,7 @@ from langchain_core.runnables import (
from langgraph.graph import END, START, StateGraph
from typing_extensions import TypedDict
from open_notebook.domain import Note, Notebook, Source
from open_notebook.domain.notebook import Note, Notebook, Source
from open_notebook.graphs.utils import run_pattern

View file

@ -1,5 +1,4 @@
import operator
import os
from typing import List, Literal, Sequence
from langchain_core.runnables import (
@ -8,6 +7,7 @@ from langchain_core.runnables import (
from langgraph.graph import END, START, StateGraph
from typing_extensions import Annotated, TypedDict
from open_notebook.config import DEFAULT_MODELS
from open_notebook.graphs.utils import run_pattern
@ -19,7 +19,7 @@ class PatternChainState(TypedDict):
def call_model(state: dict, config: RunnableConfig) -> dict:
model_name = config.get("configurable", {}).get(
"model_name", os.environ.get("DEFAULT_MODEL")
"model_name", DEFAULT_MODELS.default_transformation_model
)
transformations = state["transformations"]
current_transformation = transformations.pop(0)

View file

@ -1,11 +1,10 @@
import os
from langchain_core.runnables import (
RunnableConfig,
)
from langgraph.graph import END, START, StateGraph
from typing_extensions import TypedDict
from open_notebook.config import DEFAULT_MODELS
from open_notebook.graphs.utils import run_pattern
@ -17,7 +16,7 @@ class PatternState(TypedDict):
def call_model(state: dict, config: RunnableConfig) -> dict:
model_name = config.get("configurable", {}).get(
"model_name", os.environ.get("DEFAULT_MODEL")
"model_name", DEFAULT_MODELS.default_transformation_model
)
return {
"output": run_pattern(

View file

@ -7,6 +7,7 @@ from langchain_core.runnables import (
from langgraph.graph import END, START, StateGraph
from typing_extensions import TypedDict
from open_notebook.config import DEFAULT_MODELS
from open_notebook.graphs.utils import run_pattern
from open_notebook.utils import split_text
@ -49,7 +50,7 @@ def chunk_condition(state: TocState) -> Literal["get_chunk", END]: # type: igno
def call_model(state: TocState, config: RunnableConfig) -> dict:
model_name = config.get("configurable", {}).get(
"model_name", os.environ.get("SUMMARIZATION_MODEL")
"model_name", DEFAULT_MODELS.default_transformation_model
)
return {
"toc": run_pattern(

View file

@ -9,6 +9,7 @@ from langgraph.graph import END, START, StateGraph
from pydantic import BaseModel
from typing_extensions import TypedDict
from open_notebook.config import DEFAULT_MODELS
from open_notebook.graphs.utils import run_pattern
from open_notebook.utils import split_text
@ -57,9 +58,9 @@ def chunk_condition(state: SummaryState) -> Literal["get_chunk", END]: # type:
return END
def call_model(state: SummaryState, config: RunnableConfig) -> dict:
def call_model(state: dict, config: RunnableConfig) -> dict:
model_name = config.get("configurable", {}).get(
"model_name", os.environ.get("SUMMARIZATION_MODEL")
"model_name", DEFAULT_MODELS.default_transformation_model
)
parser = PydanticOutputParser(pydantic_object=SummaryResponse)
return {

View file

@ -1,9 +1,10 @@
import os
from langchain.output_parsers import OutputFixingParser
from loguru import logger
from open_notebook.llm_router import get_langchain_model
from open_notebook.config import DEFAULT_MODELS
from open_notebook.models import get_model
from open_notebook.prompter import Prompter
from open_notebook.utils import token_count
def run_pattern(
@ -14,24 +15,35 @@ def run_pattern(
parser=None,
output_fixing_model_name=None,
) -> dict:
if not model_name:
model_name = os.environ["DEFAULT_MODEL"]
system_prompt = Prompter(prompt_template=pattern_name, parser=parser).render(
data=state
)
chain = get_langchain_model(model_name)
tokens = token_count(str(system_prompt) + str(messages))
if tokens > 105_000 and DEFAULT_MODELS.large_context_model:
model_name = DEFAULT_MODELS.large_context_model
logger.debug(
f"Using large context model ({model_name}) because the content has {tokens} tokens"
)
logger.warning(system_prompt)
elif tokens > 105_000 and not DEFAULT_MODELS.large_context_model:
logger.critical(
f"Content has {tokens} tokens, but no large context model is configured"
)
elif not model_name:
model_name = DEFAULT_MODELS.default_transformation_model
chain = get_model(model_name, model_type="language")
if parser:
chain = chain | parser
if output_fixing_model_name and parser:
output_fix_model = get_langchain_model(output_fixing_model_name)
output_fix_model = get_model(output_fixing_model_name, model_type="language")
chain = chain | OutputFixingParser.from_llm(
parser=parser,
llm=output_fix_model,
)
system_prompt = Prompter(prompt_template=pattern_name, parser=parser).render(
data=state
)
if len(messages) > 0:
response = chain.invoke([system_prompt] + messages)
else:

View file

@ -1,35 +0,0 @@
from open_notebook.llms import (
AnthropicLanguageModel,
GeminiLanguageModel,
LiteLLMLanguageModel,
OllamaLanguageModel,
OpenAILanguageModel,
OpenRouterLanguageModel,
VertexAILanguageModel,
VertexAnthropicLanguageModel,
)
# Map provider names to classes
PROVIDER_CLASS_MAP = {
"ollama": OllamaLanguageModel,
"openrouter": OpenRouterLanguageModel,
"vertexai-anthropic": VertexAnthropicLanguageModel,
"litellm": LiteLLMLanguageModel,
"vertexai": VertexAILanguageModel,
"anthropic": AnthropicLanguageModel,
"openai": OpenAILanguageModel,
"gemini": GeminiLanguageModel,
}
def get_langchain_model(model_name, json=False):
parts = model_name.split("/")
provider = parts[0]
model_name_wihout_provider = "/".join(parts[1:])
if provider not in PROVIDER_CLASS_MAP.keys():
raise ValueError(
f"Provider {provider} not found in config. Make sure you use the correct format for model names, example: openai/gpt-4o-mini"
)
return PROVIDER_CLASS_MAP[provider](
model_name=model_name_wihout_provider, json=json
).to_langchain()

View file

@ -0,0 +1,83 @@
from open_notebook.domain.models import Model
from open_notebook.models.embedding_models import (
GeminiEmbeddingModel,
OllamaEmbeddingModel,
OpenAIEmbeddingModel,
VertexEmbeddingModel,
)
from open_notebook.models.llms import (
AnthropicLanguageModel,
GeminiLanguageModel,
LiteLLMLanguageModel,
OllamaLanguageModel,
OpenAILanguageModel,
OpenRouterLanguageModel,
VertexAILanguageModel,
VertexAnthropicLanguageModel,
)
from open_notebook.models.speech_to_text_models import OpenAISpeechToTextModel
from open_notebook.models.text_to_speech_models import (
ElevenLabsTextToSpeechModel,
OpenAITextToSpeechModel,
)
# Unified model class map with type information
MODEL_CLASS_MAP = {
"language": {
"ollama": OllamaLanguageModel,
"openrouter": OpenRouterLanguageModel,
"vertexai-anthropic": VertexAnthropicLanguageModel,
"litellm": LiteLLMLanguageModel,
"vertexai": VertexAILanguageModel,
"anthropic": AnthropicLanguageModel,
"openai": OpenAILanguageModel,
"gemini": GeminiLanguageModel,
},
"embedding": {
"openai": OpenAIEmbeddingModel,
"gemini": GeminiEmbeddingModel,
"vertexai": VertexEmbeddingModel,
"ollama": OllamaEmbeddingModel,
},
"speech_to_text": {
"openai": OpenAISpeechToTextModel,
},
"text_to_speech": {
"openai": OpenAITextToSpeechModel,
"elevenlabs": ElevenLabsTextToSpeechModel,
},
}
def get_model(model_id, model_type="language", **kwargs):
"""
Get a model instance based on model_id and type.
Args:
model_id: The ID of the model to retrieve
model_type: Type of model ('language', 'embedding', or 'speech_to_text')
**kwargs: Additional arguments to pass to the model constructor
"""
assert model_id, "Model ID cannot be empty"
model: Model = Model.get(model_id)
if not model:
raise ValueError(f"Model with ID {model_id} not found")
if model_type not in MODEL_CLASS_MAP:
raise ValueError(f"Invalid model type: {model_type}")
provider_map = MODEL_CLASS_MAP[model_type]
if model.provider not in provider_map:
raise ValueError(
f"Provider {model.provider} not compatible with {model_type} models"
)
model_class = provider_map[model.provider]
model_instance = model_class(model_name=model.name, **kwargs)
# Special handling for language models that need langchain conversion
if model_type == "language":
return model_instance.to_langchain()
return model_instance

View file

@ -0,0 +1,104 @@
"""
Classes for supporting different embedding models
"""
from __future__ import annotations
import os
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List, Optional
import requests
# todo: add support for multiple embeddings (array)
@dataclass
class EmbeddingModel(ABC):
"""
Abstract base class for language models.
"""
model_name: Optional[str] = None
@abstractmethod
def embed(self, text: str) -> List[float]:
"""
Generates an embedding
"""
raise NotImplementedError
@dataclass
class OllamaEmbeddingModel(EmbeddingModel):
model_name: str
base_url: str = os.environ.get("OLLAMA_API_BASE", "http://localhost:11434")
def embed(self, text: str) -> List[float]:
"""
Embeds the content using Open AI embedding
"""
text = text.replace("\n", " ")
response = requests.post(
f"{self.base_url}/api/embed",
json={"model": self.model_name, "input": [text]},
)
return response.json()["embeddings"][0]
@dataclass
class GeminiEmbeddingModel(EmbeddingModel):
model_name: str
def embed(self, text: str) -> List[float]:
import google.generativeai as genai
"""
Embeds the content using Open AI embedding
"""
model_name = (
self.model_name
if self.model_name.startswith("models/")
else f"models/{self.model_name}"
)
result = genai.embed_content(model=model_name, content=text)
return result["embedding"]
@dataclass
class VertexEmbeddingModel(EmbeddingModel):
model_name: str
def embed(self, text: str) -> List[float]:
from vertexai.language_models import TextEmbeddingInput, TextEmbeddingModel
texts = [text]
# The dimensionality of the output embeddings.
# dimensionality = 256
# The task type for embedding. Check the available tasks in the model's documentation.
model = TextEmbeddingModel.from_pretrained(self.model_name)
inputs = [TextEmbeddingInput(text) for text in texts]
embeddings = model.get_embeddings(inputs)
return embeddings[0].values
@dataclass
class OpenAIEmbeddingModel(EmbeddingModel):
model_name: str
def embed(self, text: str) -> List[float]:
from openai import OpenAI
"""
Embeds the content using Open AI embedding
"""
# todo: make this Singleton
client = OpenAI()
text = text.replace("\n", " ")
return (
client.embeddings.create(input=[text], model=self.model_name)
.data[0]
.embedding
)

View file

@ -1,5 +1,5 @@
"""
Classes for supporting different language and vector models
Classes for supporting different language models
"""
import os
@ -15,9 +15,9 @@ from langchain_google_vertexai import ChatVertexAI
from langchain_google_vertexai.model_garden import ChatAnthropicVertex
from langchain_ollama.chat_models import ChatOllama
from langchain_openai.chat_models import ChatOpenAI
from pydantic import SecretStr
# from redisvl.utils.vectorize import BaseVectorizer
# from redisvl.utils.vectorize.text.openai import OpenAITextVectorizer
# future: is there a value on returning langchain specific models?
@dataclass
@ -186,7 +186,7 @@ class OpenRouterLanguageModel(LanguageModel):
max_tokens=self.max_tokens,
model_kwargs=kwargs,
streaming=self.streaming,
api_key=os.environ.get("OPENROUTER_API_KEY", "openrouter"),
api_key=SecretStr(os.environ.get("OPENROUTER_API_KEY", "openrouter")),
top_p=self.top_p,
)
@ -238,28 +238,3 @@ class OpenAILanguageModel(LanguageModel):
streaming=self.streaming,
top_p=self.top_p,
)
# @dataclass
# class EmbeddingModel(ABC):
# model_name: str
# dimensions: int
# def to_redis_vectorizer(self) -> BaseVectorizer:
# raise NotImplementedError
# @dataclass
# class OpenAIEmbeddingModel(EmbeddingModel):
# """
# Embedding model that uses the OpenAI text embedding model.
# """
# model_name: str
# dimensions: int
# def to_redis_vectorizer(self) -> OpenAITextVectorizer:
# """
# Convert the embedding model to a Redis vectorizer.
# """
# return OpenAITextVectorizer(model=self.model_name)

View file

@ -0,0 +1,42 @@
"""
Classes for supporting different transcription models
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional
@dataclass
class SpeechToTextModel(ABC):
"""
Abstract base class for speech to text models.
"""
model_name: Optional[str] = None
@abstractmethod
def transcribe(self, audio_file_path: str) -> str:
"""
Generates a text transcription from audio
"""
raise NotImplementedError
@dataclass
class OpenAISpeechToTextModel(SpeechToTextModel):
model_name: str
def transcribe(self, audio_file_path: str) -> str:
"""
Transcribes an audio file into text
"""
from openai import OpenAI
# todo: make this Singleton
client = OpenAI()
with open(audio_file_path, "rb") as audio:
transcription = client.audio.transcriptions.create(
model=self.model_name, file=audio
)
return transcription.text

View file

@ -0,0 +1,26 @@
"""
Classes for supporting different text to speech models
"""
from abc import ABC
from dataclasses import dataclass
from typing import Optional
@dataclass
class TextToSpeechModel(ABC):
"""
Abstract base class for text to speech models.
"""
model_name: Optional[str] = None
@dataclass
class OpenAITextToSpeechModel(TextToSpeechModel):
model_name: str
@dataclass
class ElevenLabsTextToSpeechModel(TextToSpeechModel):
model_name: str

View file

@ -1,10 +1,10 @@
from typing import ClassVar, List, Literal, Optional
from typing import ClassVar, List, Optional
from loguru import logger
from podcastfy.client import generate_podcast
from pydantic import Field, field_validator
from open_notebook.domain import ObjectModel
from open_notebook.domain.notebook import ObjectModel
class PodcastEpisode(ObjectModel):
@ -31,7 +31,7 @@ class PodcastConfig(ObjectModel):
ending_message: Optional[str] = None
wordcount: int = Field(ge=400, le=10000)
creativity: float = Field(ge=0, le=1)
provider: Literal["openai", "elevenlabs", "edge"] = Field(default="openai")
provider: str = Field(default="openai")
voice1: Optional[str] = None
voice2: Optional[str] = None
model: str

View file

@ -6,11 +6,8 @@ from urllib.parse import urlparse
import requests
import tomli
from langchain_text_splitters import CharacterTextSplitter
from openai import OpenAI
from packaging.version import parse as parse_version
client = OpenAI()
def split_text(txt: str, chunk=1000, overlap=0, separator=" "):
"""
@ -63,21 +60,6 @@ def token_cost(token_count, cost_per_million=0.150):
return cost_per_million * (token_count / 1_000_000)
def get_embedding(text, model="text-embedding-3-small"):
"""
Get the embedding for the input text using the specified model.
Args:
text (str): The input text to get the embedding for.
model (str): The name of the embedding model to use. Default is "text-embedding-3-small".
Returns:
list: The embedding vector for the input text.
"""
text = text.replace("\n", " ")
return client.embeddings.create(input=[text], model=model).data[0].embedding
def remove_non_ascii(text):
return re.sub(r"[^\x00-\x7F]+", "", text)

View file

@ -1,7 +1,7 @@
import streamlit as st
from humanize import naturaltime
from open_notebook.domain import Notebook
from open_notebook.domain.notebook import Notebook
from stream_app.chat import chat_sidebar
from stream_app.note import add_note, note_card
from stream_app.source import add_source, source_card

View file

@ -1,7 +1,7 @@
import streamlit as st
from open_notebook.domain import text_search, vector_search
from open_notebook.utils import get_embedding
from open_notebook.config import EMBEDDING_MODEL
from open_notebook.domain.notebook import text_search, vector_search
from stream_app.note import note_list_item
from stream_app.source import source_list_item
from stream_app.utils import version_sidebar
@ -33,7 +33,7 @@ with st.container(border=True):
)
elif search_type == "Vector Search":
st.write(f"Searching for {search_term}")
embed_query = get_embedding(search_term)
embed_query = EMBEDDING_MODEL.embed(search_term)
st.session_state["search_results"] = vector_search(
embed_query, 100, search_sources, search_notes
)

View file

@ -1,6 +1,9 @@
from typing import Dict, List
import streamlit as st
from streamlit_tags import st_tags
from open_notebook.domain.models import Model
from open_notebook.plugins.podcasts import (
PodcastConfig,
PodcastEpisode,
@ -17,6 +20,21 @@ st.set_page_config(
version_sidebar()
text_to_speech_models = Model.get_models_by_type("text_to_speech")
provider_models: Dict[str, List[str]] = {}
for model in text_to_speech_models:
if model.provider not in provider_models:
provider_models[model.provider] = []
provider_models[model.provider].append(model.name)
if len(text_to_speech_models) == 0:
st.error("No text to speech models found. Please set one up in the Settings page.")
st.stop()
episodes_tab, templates_tab = st.tabs(["Episodes", "Templates"])
with episodes_tab:
@ -76,7 +94,7 @@ with templates_tab:
pd_cfg["ending_message"] = st.text_input(
"Ending Message", placeholder="Thank you for listening!"
)
pd_cfg["provider"] = st.selectbox("Provider", ["openai", "elevenlabs", "edge"])
pd_cfg["provider"] = st.selectbox("Provider", provider_models.keys())
pd_cfg["voice1"] = st.text_input(
"Voice 1", help="You can use Elevenlabs voice ID"
)
@ -86,7 +104,8 @@ with templates_tab:
pd_cfg["voice2"] = st.text_input(
"Voice 2", help="You can use Elevenlabs voice ID"
)
pd_cfg["model"] = st.text_input("Model")
pd_cfg["model"] = st.selectbox("Model", provider_models[pd_cfg["provider"]])
st.caption(
"OpenAI: tts-1 or tts-1-hd, Elevenlabs: eleven_multilingual_v2, eleven_turbo_v2_5"
)
@ -183,8 +202,8 @@ with templates_tab:
)
pd_config.provider = st.selectbox(
"Provider",
["openai", "elevenlabs", "edge"],
index=["openai", "elevenlabs", "edge"].index(pd_config.provider),
list(provider_models.keys()),
index=list(provider_models.keys()).index(pd_config.provider),
key=f"provider_{pd_config.id}",
)
pd_config.voice1 = st.text_input(
@ -202,8 +221,11 @@ with templates_tab:
key=f"voice2_{pd_config.id}",
help="You can use Elevenlabs voice ID",
)
pd_config.model = st.text_input(
"Model", value=pd_config.model, key=f"model_{pd_config.id}"
pd_config.model = st.selectbox(
"Model",
provider_models[pd_config.provider],
index=provider_models[pd_config.provider].index(pd_config.model),
key=f"model_{pd_config.id}",
)
st.caption(
"OpenAI: tts-1 or tts-1-hd, Elevenlabs: eleven_multilingual_v2, eleven_turbo_v2_5"

222
pages/9_⚙️_Settings.py Normal file
View file

@ -0,0 +1,222 @@
import os
import streamlit as st
from open_notebook.domain.models import DefaultModels, Model
from open_notebook.models import MODEL_CLASS_MAP
from stream_app.utils import version_sidebar
st.set_page_config(
layout="wide", page_title="⚙️ Settings", initial_sidebar_state="expanded"
)
version_sidebar()
st.title("Settings")
model_tab, model_defaults_tab = st.tabs(["Models", "Model Defaults"])
provider_status = {}
model_types = [
# "vision",
"language",
"embedding",
"text_to_speech",
"speech_to_text",
]
provider_status["ollama"] = os.environ.get("OLLAMA_API_BASE") is not None
provider_status["openai"] = os.environ.get("OPENAI_API_KEY") is not None
provider_status["vertexai"] = (
os.environ.get("VERTEX_PROJECT") is not None
and os.environ.get("VERTEX_LOCATION") is not None
and os.environ.get("GOOGLE_APPLICATION_CREDENTIALS") is not None
)
provider_status["vertexai-anthropic"] = (
os.environ.get("VERTEX_PROJECT") is not None
and os.environ.get("VERTEX_LOCATION") is not None
and os.environ.get("GOOGLE_APPLICATION_CREDENTIALS") is not None
)
provider_status["gemini"] = os.environ.get("GEMINI_API_KEY") is not None
provider_status["openrouter"] = (
os.environ.get("OPENROUTER_API_KEY") is not None
and os.environ.get("OPENAI_API_KEY") is not None
and os.environ.get("OPENROUTER_BASE_URL") is not None
)
provider_status["anthropic"] = os.environ.get("ANTHROPIC_API_KEY") is not None
provider_status["elevenlabs"] = os.environ.get("ELEVENLABS_API_KEY") is not None
provider_status["litellm"] = (
provider_status["ollama"]
or provider_status["vertexai"]
or provider_status["vertexai-anthropic"]
or provider_status["anthropic"]
or provider_status["openai"]
or provider_status["gemini"]
)
available_providers = [k for k, v in provider_status.items() if v]
unavailable_providers = [k for k, v in provider_status.items() if not v]
with model_tab:
st.subheader("Add Model")
provider = st.selectbox("Provider", available_providers)
if len(unavailable_providers) > 0:
st.caption(
f"Unavailable Providers: {', '.join(unavailable_providers)}. Please check docs page if you wish to enable them."
)
# Filter model types based on provider availability in MODEL_CLASS_MAP
available_model_types = []
for model_type in model_types:
if model_type in MODEL_CLASS_MAP and provider in MODEL_CLASS_MAP[model_type]:
available_model_types.append(model_type)
if not available_model_types:
st.error(f"No compatible model types available for provider: {provider}")
else:
model_type = st.selectbox(
"Model Type",
available_model_types,
help="Use language for text generation models, text_to_speech for TTS models for generating podcasts, etc.",
)
model_name = st.text_input(
"Model Name", "", help="gpt-4o-mini, claude, gemini, llama3, etc"
)
if st.button("Save"):
model = Model(name=model_name, provider=provider, type=model_type)
model.save()
st.success("Saved")
st.divider()
all_models = Model.get_all()
st.subheader("Configured Models")
model_types_available = {
# "vision": False,
"language": False,
"embedding": False,
"text_to_speech": False,
"speech_to_text": False,
}
for model in all_models:
model_types_available[model.type] = True
with st.container(border=True):
st.markdown(f"{model.name} ({model.provider}, {model.type})")
if st.button("Delete", key=f"delete_{model.id}"):
model.delete()
st.rerun()
for model_type, available in model_types_available.items():
if not available:
st.warning(f"No models available for {model_type}")
def get_selected_index(models, model_id, default=0):
"""Returns the index of the selected model in the list of models"""
if not model_id or not models:
return default
for i, model in enumerate(models):
if model.id == model_id:
return i
return default
with model_defaults_tab:
default_models = DefaultModels.load().model_dump()
all_models = Model.get_all()
text_generation_models = [model for model in all_models if model.type == "language"]
text_to_speech_models = [
model for model in all_models if model.type == "text_to_speech"
]
speech_to_text_models = [
model for model in all_models if model.type == "speech_to_text"
]
vision_models = [model for model in all_models if model.type == "vision"]
embedding_models = [model for model in all_models if model.type == "embedding"]
st.write(
"In this section, you can select the default models to be used on the various content operations done by Open Notebook. Some of these can be overriden in the different modules."
)
defs = {}
defs["default_chat_model"] = st.selectbox(
"Default Chat Model",
text_generation_models,
format_func=lambda x: x.name,
help="This model will be used for chat.",
index=get_selected_index(
text_generation_models, default_models.get("default_chat_model")
),
)
st.divider()
defs["default_transformation_model"] = st.selectbox(
"Default Transformation Model",
text_generation_models,
format_func=lambda x: x.name,
help="This model will be used for text transformations such as summaries, insights, etc.",
index=get_selected_index(
text_generation_models, default_models.get("default_transformation_model")
),
)
st.caption("You can override this model on individual transformations")
st.divider()
defs["large_context_model"] = st.selectbox(
"Large Context Model",
text_generation_models,
format_func=lambda x: x.name,
help="This model will be used for larger context generation -- recommended: Gemini",
index=get_selected_index(
text_generation_models, default_models.get("large_context_model")
),
)
st.caption("Recommended to use Gemini models for larger context processing")
st.divider()
defs["default_text_to_speech_model"] = st.selectbox(
"Default Text to Speech Model",
text_to_speech_models,
format_func=lambda x: x.name,
help="This is the default model for converting text to speech (podcasts, etc)",
index=get_selected_index(
text_to_speech_models, default_models.get("default_text_to_speech_model")
),
)
st.caption("You can override this model on different podcasts")
st.divider()
defs["default_speech_to_text_model"] = st.selectbox(
"Default Speech to Text Model",
speech_to_text_models,
format_func=lambda x: x.name,
help="This is the default model for converting speech to text (audio transcriptions, etc)",
index=get_selected_index(
speech_to_text_models, default_models.get("default_speech_to_text_model")
),
)
st.divider()
# defs["default_vision_model"] = st.selectbox(
# "Default Vision Model",
# vision_models,
# format_func=lambda x: x.name,
# help="This is the default model for vision tasks (image recognition, PDF recognition, etc)",
# index=get_selected_index(
# vision_models, default_models.get("default_vision_model")
# ),
# )
# st.divider()
defs["default_embedding_model"] = st.selectbox(
"Default Embedding Model",
embedding_models,
format_func=lambda x: x.name,
help="This is the default model for embeddings (semantic search, etc)",
index=get_selected_index(
embedding_models, default_models.get("default_embedding_model")
),
)
st.caption(
"Caution: you cannot change the embedding model once there is embeddings or they will need to be regenerated"
)
# if st.button("Save Defaults", key="save_defaults"):
for k, v in defs.items():
if v:
defs[k] = v.id
DefaultModels.update(defs)

45
poetry.lock generated
View file

@ -1932,6 +1932,27 @@ qtconsole = ["qtconsole"]
test = ["packaging", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"]
test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"]
[[package]]
name = "ipywidgets"
version = "8.1.5"
description = "Jupyter interactive widgets"
optional = false
python-versions = ">=3.7"
files = [
{file = "ipywidgets-8.1.5-py3-none-any.whl", hash = "sha256:3290f526f87ae6e77655555baba4f36681c555b8bdbbff430b70e52c34c86245"},
{file = "ipywidgets-8.1.5.tar.gz", hash = "sha256:870e43b1a35656a80c18c9503bbf2d16802db1cb487eec6fab27d683381dde17"},
]
[package.dependencies]
comm = ">=0.1.3"
ipython = ">=6.1.0"
jupyterlab-widgets = ">=3.0.12,<3.1.0"
traitlets = ">=4.3.1"
widgetsnbextension = ">=4.0.12,<4.1.0"
[package.extras]
test = ["ipykernel", "jsonschema", "pytest (>=3.6.0)", "pytest-cov", "pytz"]
[[package]]
name = "jedi"
version = "0.19.1"
@ -2163,6 +2184,17 @@ files = [
{file = "jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d"},
]
[[package]]
name = "jupyterlab-widgets"
version = "3.0.13"
description = "Jupyter interactive widgets for JupyterLab"
optional = false
python-versions = ">=3.7"
files = [
{file = "jupyterlab_widgets-3.0.13-py3-none-any.whl", hash = "sha256:e3cda2c233ce144192f1e29914ad522b2f4c40e77214b0cc97377ca3d323db54"},
{file = "jupyterlab_widgets-3.0.13.tar.gz", hash = "sha256:a2966d385328c1942b683a8cd96b89b8dd82c8b8f81dda902bb2bc06d46f5bed"},
]
[[package]]
name = "langchain"
version = "0.3.4"
@ -6167,6 +6199,17 @@ files = [
[package.extras]
test = ["pytest (>=6.0.0)", "setuptools (>=65)"]
[[package]]
name = "widgetsnbextension"
version = "4.0.13"
description = "Jupyter interactive widgets for Jupyter Notebook"
optional = false
python-versions = ">=3.7"
files = [
{file = "widgetsnbextension-4.0.13-py3-none-any.whl", hash = "sha256:74b2692e8500525cc38c2b877236ba51d34541e6385eeed5aec15a70f88a6c71"},
{file = "widgetsnbextension-4.0.13.tar.gz", hash = "sha256:ffcb67bc9febd10234a362795f643927f4e0c05d9342c727b65d2384f8feacb6"},
]
[[package]]
name = "win32-setctime"
version = "1.1.0"
@ -6324,4 +6367,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "4fa191c6df5a7a355eb0d61f9560ec70e4671ac49cd54fa3a166c1e25c325671"
content-hash = "265ed7b26b19c54847b8e549f09ccbf8be68120b34f392fb5b8afc9ffccd62ac"

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "open-notebook"
version = "0.0.6"
version = "0.0.7"
description = "An open source implementation of a research assistant, inspired by Google Notebook LM"
authors = ["Luis Novo <lfnovo@gmail.com>"]
license = "MIT"
@ -46,12 +46,14 @@ bs4 = "^0.0.2"
python-docx = "^1.1.2"
python-pptx = "^1.0.2"
openpyxl = "^3.1.5"
google-generativeai = "^0.8.3"
[tool.poetry.group.dev.dependencies]
ipykernel = "^6.29.5"
ruff = "^0.5.5"
mypy = "^1.11.1"
types-requests = "^2.32.0.20241016"
ipywidgets = "^8.1.5"
[build-system]
requires = ["poetry-core"]

View file

@ -1,7 +1,7 @@
import streamlit as st
from langchain_core.runnables import RunnableConfig
from open_notebook.domain import Note, Source
from open_notebook.domain.notebook import Note, Source
from open_notebook.graphs.chat import graph as chat_graph
from open_notebook.plugins.podcasts import PodcastConfig
from open_notebook.utils import token_count
@ -54,8 +54,6 @@ def execute_chat(txt_input, session_id):
return result
# todo: se eu for usar o token count, preciso deixar configuravel
# seria bom ter um total de tokens no admin em algum lugar
def chat_sidebar(session_id):
context = build_context(session_id=session_id)
tokens = token_count(str(context) + str(st.session_state[session_id]["messages"]))

View file

@ -3,7 +3,7 @@ from humanize import naturaltime
from loguru import logger
from streamlit_monaco import st_monaco # type: ignore
from open_notebook.domain import Note
from open_notebook.domain.notebook import Note
from open_notebook.graphs.multipattern import graph as pattern_graph
from open_notebook.utils import surreal_clean

View file

@ -8,7 +8,7 @@ from humanize import naturaltime
from loguru import logger
from open_notebook.config import UPLOADS_FOLDER
from open_notebook.domain import Asset, Source
from open_notebook.domain.notebook import Asset, Source
from open_notebook.exceptions import UnsupportedTypeException
from open_notebook.graphs.content_processing import graph
from open_notebook.graphs.multipattern import graph as transform_graph