bookworm_genai 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.1
2
+ Name: bookworm_genai
3
+ Version: 0.1.0
4
+ Summary: Bookworm - A LLM-powered bookmark search engine
5
+ Author: kiran94
6
+ Requires-Python: >=3.9,<4.0
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.9
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Requires-Dist: duckdb (>=1.0.0,<2.0.0)
13
+ Requires-Dist: jq (>=1.7.0,<2.0.0)
14
+ Requires-Dist: langchain (>=0.2.12,<0.3.0)
15
+ Requires-Dist: langchain-community (>=0.2.11,<0.3.0)
16
+ Requires-Dist: langchain-openai (>=0.1.20,<0.2.0)
17
+ Requires-Dist: pandas (>=2.2.2,<3.0.0)
18
+ Requires-Dist: platformdirs (>=4.2.2,<5.0.0)
19
+ Requires-Dist: rich (>=13.7.1,<14.0.0)
20
+ Description-Content-Type: text/markdown
21
+
22
+ # bookworm
23
+
24
+ [![main](https://github.com/kiran94/bookworm/actions/workflows/main.yml/badge.svg)](https://github.com/kiran94/bookworm/actions/workflows/main.yml)
25
+
26
+ ### Processes
27
+
28
+ *`bookworm sync`*
29
+
30
+ ```python
31
+ python -m bookworm sync
32
+ ```
33
+
34
+ ```mermaid
35
+ graph LR
36
+
37
+ subgraph Bookmarks
38
+ Chrome(Chrome Bookmarks)
39
+ Brave(Brave Bookmarks)
40
+ end
41
+
42
+ Bookworm(bookworm sync)
43
+
44
+ EmbeddingsService(Embeddings Service e.g OpenAIEmbeddings)
45
+
46
+ VectorStore(Vector Store e.g DuckDB)
47
+
48
+ Chrome -->|load bookmarks|Bookworm
49
+ Brave -->|load bookmarks|Bookworm
50
+
51
+ Bookworm -->|vectorize bookmarks|EmbeddingsService-->|store embeddings|VectorStore
52
+ ```
53
+
54
+ ---
55
+
56
+ *`bookworm ask`*
57
+
58
+ ```python
59
+ python -m bookworm ask
60
+ ```
61
+
62
+ ```mermaid
63
+ graph LR
64
+
65
+ query
66
+ Bookworm(bookworm ask)
67
+
68
+ subgraph _
69
+ LLM(LLM e.g OpenAI)
70
+ VectorStore(Vector Store e.g DuckDB)
71
+ end
72
+
73
+ query -->|user queries for information|Bookworm
74
+
75
+ Bookworm -->|simularity search|VectorStore -->|send similar docs + user query|LLM
76
+ LLM -->|send back response|Bookworm
77
+ ```
78
+
79
+ ---
80
+
81
+
82
+ ### Developer Setup
83
+
84
+ ```bash
85
+ # LLMs
86
+ export OPENAI_API_KEY=
87
+
88
+ # Langchain (optional, but useful for debugging)
89
+ export LANGCHAIN_API_KEY=
90
+ export LANGCHAIN_TRACING_V2=true
91
+ export LANGCHAIN_PROJECT=bookworm
92
+
93
+ # Misc (optional)
94
+ export LOGGING_LEVEL=INFO
95
+ ```
96
+
97
+ Recommendations:
98
+
99
+ - Install [`pyenv`](https://github.com/pyenv/pyenv?tab=readme-ov-file#installation) and ensure [build dependencies are installed](https://github.com/pyenv/pyenv?tab=readme-ov-file#install-python-build-dependencies) for your OS.
100
+ - Install [Poetry](https://python-poetry.org/docs/) we will be using [environment management](https://python-poetry.org/docs/managing-environments/) below.
101
+
102
+
103
+ ```bash
104
+ poetry env use 3.9 # or path to your 3.9 installation
105
+
106
+ poetry shell
107
+ poetry install
108
+
109
+ bookworm --help
110
+ ```
111
+
@@ -0,0 +1,89 @@
1
+ # bookworm
2
+
3
+ [![main](https://github.com/kiran94/bookworm/actions/workflows/main.yml/badge.svg)](https://github.com/kiran94/bookworm/actions/workflows/main.yml)
4
+
5
+ ### Processes
6
+
7
+ *`bookworm sync`*
8
+
9
+ ```python
10
+ python -m bookworm sync
11
+ ```
12
+
13
+ ```mermaid
14
+ graph LR
15
+
16
+ subgraph Bookmarks
17
+ Chrome(Chrome Bookmarks)
18
+ Brave(Brave Bookmarks)
19
+ end
20
+
21
+ Bookworm(bookworm sync)
22
+
23
+ EmbeddingsService(Embeddings Service e.g OpenAIEmbeddings)
24
+
25
+ VectorStore(Vector Store e.g DuckDB)
26
+
27
+ Chrome -->|load bookmarks|Bookworm
28
+ Brave -->|load bookmarks|Bookworm
29
+
30
+ Bookworm -->|vectorize bookmarks|EmbeddingsService-->|store embeddings|VectorStore
31
+ ```
32
+
33
+ ---
34
+
35
+ *`bookworm ask`*
36
+
37
+ ```python
38
+ python -m bookworm ask
39
+ ```
40
+
41
+ ```mermaid
42
+ graph LR
43
+
44
+ query
45
+ Bookworm(bookworm ask)
46
+
47
+ subgraph _
48
+ LLM(LLM e.g OpenAI)
49
+ VectorStore(Vector Store e.g DuckDB)
50
+ end
51
+
52
+ query -->|user queries for information|Bookworm
53
+
54
+ Bookworm -->|simularity search|VectorStore -->|send similar docs + user query|LLM
55
+ LLM -->|send back response|Bookworm
56
+ ```
57
+
58
+ ---
59
+
60
+
61
+ ### Developer Setup
62
+
63
+ ```bash
64
+ # LLMs
65
+ export OPENAI_API_KEY=
66
+
67
+ # Langchain (optional, but useful for debugging)
68
+ export LANGCHAIN_API_KEY=
69
+ export LANGCHAIN_TRACING_V2=true
70
+ export LANGCHAIN_PROJECT=bookworm
71
+
72
+ # Misc (optional)
73
+ export LOGGING_LEVEL=INFO
74
+ ```
75
+
76
+ Recommendations:
77
+
78
+ - Install [`pyenv`](https://github.com/pyenv/pyenv?tab=readme-ov-file#installation) and ensure [build dependencies are installed](https://github.com/pyenv/pyenv?tab=readme-ov-file#install-python-build-dependencies) for your OS.
79
+ - Install [Poetry](https://python-poetry.org/docs/) we will be using [environment management](https://python-poetry.org/docs/managing-environments/) below.
80
+
81
+
82
+ ```bash
83
+ poetry env use 3.9 # or path to your 3.9 installation
84
+
85
+ poetry shell
86
+ poetry install
87
+
88
+ bookworm --help
89
+ ```
@@ -0,0 +1,17 @@
1
+ import logging
2
+ import os
3
+
4
+ from rich.logging import RichHandler
5
+
6
+ LOGGING_LEVEL = os.environ.get("LOGGING_LEVEL", logging.INFO)
7
+ LOGGING_FORMAT = os.environ.get("LOGGING_FORMAT", "%(message)s")
8
+
9
+ _is_debug = logging.getLevelName(LOGGING_LEVEL) == logging.DEBUG
10
+
11
+ logging.basicConfig(
12
+ level=LOGGING_LEVEL,
13
+ format=LOGGING_FORMAT,
14
+ handlers=[RichHandler(markup=True, show_path=_is_debug, show_time=_is_debug, show_level=_is_debug)],
15
+ )
16
+
17
+ logging.getLogger("httpx").setLevel(logging.WARNING)
@@ -0,0 +1,70 @@
1
+ import sys
2
+ import logging
3
+ import argparse
4
+
5
+ from bookworm_genai.commands.sync import sync
6
+ from bookworm_genai.commands.ask import BookmarkChain
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def main():
12
+ logger.info("[bold green]Starting Bookworm 📖")
13
+ logger.debug("Running on platform '%s'", sys.platform)
14
+
15
+ argparser = argparse.ArgumentParser(description="Bookworm - A LLM-powered bookmark search engine")
16
+
17
+ sub_parsers = argparser.add_subparsers(dest="command", help="Available commands")
18
+ sub_parsers.add_parser("sync", help="Sync the bookmark database with the latest changes")
19
+ sub_parsers.add_parser("ask", help="Search for a bookmark")
20
+
21
+ args = argparser.parse_args()
22
+
23
+ logger.debug("Arguments: %s", args)
24
+
25
+ if args.command == "sync":
26
+ sync()
27
+
28
+ elif args.command == "ask":
29
+ logger.info("What would you like to search for?")
30
+ query = input("> ")
31
+
32
+ logger.debug("query: %s", query)
33
+
34
+ with BookmarkChain() as bookmark_chain:
35
+ if not bookmark_chain.is_valid():
36
+ logger.debug("bookmark chain is not valid, exiting early.")
37
+ return
38
+
39
+ bookmarks = bookmark_chain.ask(query)
40
+
41
+ if not bookmarks.bookmarks:
42
+ logger.info("""
43
+ No bookmarks found for the query 🙁. Please ensure you have performed a "bookworm sync" to update the database
44
+ and the query is relevant to the bookmarks stored.
45
+ """)
46
+ return
47
+
48
+ for index, bookmark in enumerate(bookmarks.bookmarks):
49
+ if logger.isEnabledFor(logging.DEBUG):
50
+ # also shows the source of the bookmark
51
+ logger.info(f"[green][{index}] [/] {bookmark.title} - [link={bookmark.url}]{bookmark.url}[/link] ({bookmark.source})")
52
+ else:
53
+ logger.info(f"[green][{index}] [/] {bookmark.title} - [link={bookmark.url}]{bookmark.url}[/link]")
54
+
55
+ logger.info("Press a number to open the bookmark:")
56
+ while True:
57
+ try:
58
+ raw_input = input("> ")
59
+ selected_index = int(raw_input)
60
+ bookmarks.bookmarks[selected_index].open()
61
+
62
+ break
63
+ except ValueError:
64
+ logger.warning(f"Invalid input: '{raw_input}'. Please enter a number.")
65
+ except IndexError:
66
+ logger.warning(f"Invalid index: '{selected_index}'. Please select a valid index.")
67
+
68
+
69
+ if __name__ == "__main__":
70
+ main()
@@ -0,0 +1,86 @@
1
+ import os
2
+ import logging
3
+
4
+ import duckdb
5
+ from langchain_community.vectorstores import DuckDB as DuckDBVectorStore
6
+ from langchain_openai import ChatOpenAI, AzureChatOpenAI
7
+ from langchain_core.prompts import ChatPromptTemplate
8
+ from langchain_core.runnables import RunnablePassthrough
9
+ from langchain_core.language_models.chat_models import BaseChatModel
10
+
11
+ from bookworm_genai.models import Bookmarks
12
+ from bookworm_genai.storage import full_database_path, _get_embedding_store
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ _system_message = """
18
+ You have knowledge about all the browser bookmarks stored by an individual.
19
+ When a user asks a question, you should be able to search the bookmarks and return the most relevant bookmark title and URL.
20
+ It could be multiple bookmarks.
21
+ If you don't have anything in the context then return empty list
22
+
23
+ The bookmarks available are from the context:
24
+ {context}
25
+ """
26
+
27
+
28
+ class BookmarkChain:
29
+ def __init__(self):
30
+ logger.debug("Connecting to vector database at: %s", full_database_path)
31
+ self._duckdb_connection = duckdb.connect(full_database_path, read_only=False)
32
+ self.vector_store = DuckDBVectorStore(connection=self._duckdb_connection, embedding=_get_embedding_store())
33
+
34
+ llm = _get_llm()
35
+ llm = llm.with_structured_output(Bookmarks)
36
+
37
+ prompt = ChatPromptTemplate.from_messages([("system", _system_message), ("human", "{query}")])
38
+
39
+ self.chain = {"context": self.vector_store.as_retriever(), "query": RunnablePassthrough()} | prompt | llm
40
+
41
+ def ask(self, query: str) -> Bookmarks:
42
+ logger.debug("Searching for bookmarks with query: %s", query)
43
+
44
+ return self.chain.invoke(query)
45
+
46
+ def is_valid(self) -> bool:
47
+ res = self._duckdb_connection.execute("SELECT COUNT(*) FROM embeddings").fetchall()
48
+
49
+ try:
50
+ res = res[0][0]
51
+ except IndexError:
52
+ logger.warning("validation check failed due to unexpected response from the database.")
53
+ logger.debug("Raw DuckDB Response: %s", res)
54
+
55
+ return False
56
+
57
+ if res == 0:
58
+ logger.warning("No bookmarks were found in database. Please ensure you run 'bookworm sync' before asking questions")
59
+ return False
60
+ else:
61
+ return True
62
+
63
+ def __enter__(self):
64
+ return self
65
+
66
+ def __exit__(self, exc_type, exc_val, exc_tb):
67
+ logger.debug("Closing DuckDB connection")
68
+
69
+ self._duckdb_connection.close()
70
+
71
+
72
+ def _get_llm() -> BaseChatModel:
73
+ kwargs = {
74
+ "temperature": 0.0,
75
+ }
76
+
77
+ if os.environ.get("OPENAI_API_KEY"):
78
+ # https://api.python.langchain.com/en/latest/chat_models/langchain_openai.chat_models.base.ChatOpenAI.html
79
+ return ChatOpenAI(**kwargs)
80
+
81
+ elif os.environ.get("AZURE_OPENAI_API_KEY"):
82
+ # https://api.python.langchain.com/en/latest/chat_models/langchain_openai.chat_models.azure.AzureChatOpenAI.html
83
+ return AzureChatOpenAI(**kwargs)
84
+
85
+ else:
86
+ raise ValueError("No OpenAI API key found in environment variables")
@@ -0,0 +1,39 @@
1
+ import os
2
+ import sys
3
+ import logging
4
+
5
+ from langchain_community.document_loaders import JSONLoader
6
+
7
+ from bookworm_genai.integrations import browsers
8
+ from bookworm_genai.storage import store_documents
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def sync():
14
+ docs = []
15
+
16
+ for browser, config in browsers.items():
17
+ try:
18
+ platform_config = config[sys.platform]
19
+ except KeyError:
20
+ logger.warning(f"Platform {sys.platform} not supported for browser {browser}")
21
+ continue
22
+ else:
23
+ expanded_path = os.path.expanduser(platform_config["bookmark_file_path"])
24
+
25
+ jq_command = """
26
+ [.roots.bookmark_bar.children, .roots.other.children] |
27
+ flatten |
28
+ .. |
29
+ objects |
30
+ select(.type == "url")
31
+ """
32
+
33
+ logger.info("Loading bookmarks from %s", expanded_path)
34
+ bookmark_bar = JSONLoader(expanded_path, jq_command, text_content=False)
35
+ docs.extend(bookmark_bar.lazy_load())
36
+
37
+ logger.debug(f"{len(docs)} Bookmarks loaded")
38
+
39
+ store_documents(docs)
@@ -0,0 +1,26 @@
1
+ from enum import Enum
2
+
3
+
4
+ class Browser(str, Enum):
5
+ BRAVE = "brave"
6
+ CHROME = "chrome"
7
+
8
+
9
+ # Configuration for various browsers and details about them
10
+ # The bookmark_file_path is the path to the bookmarks file for the browsers, in order for it to be used it must be used in conjunction with
11
+ # os.path.expanduser as it may contain environment variables
12
+ #
13
+ # The platform configuration is keyed off the values from https://docs.python.org/3/library/sys.html#sys.platform
14
+ #
15
+ browsers = {
16
+ Browser.BRAVE: {
17
+ "linux": {"bookmark_file_path": "~/.config/BraveSoftware/Brave-Browser/Default/Bookmarks"},
18
+ "win32": {},
19
+ "darwin": {},
20
+ },
21
+ Browser.CHROME: {
22
+ "linux": {"bookmark_file_path": "~/.config/google-chrome/Default/Bookmarks"},
23
+ "win32": {},
24
+ "darwin": {},
25
+ },
26
+ }
@@ -0,0 +1,36 @@
1
+ import sys
2
+ import subprocess
3
+ import logging
4
+
5
+ from langchain_core.pydantic_v1 import BaseModel, Field
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class Bookmark(BaseModel):
11
+ """
12
+ A bookmark to a website
13
+ """
14
+
15
+ title: str = Field(description="The title of the bookmark")
16
+ url: str = Field(description="The URL of the bookmark")
17
+ source: str = Field(description="The source of the bookmark")
18
+
19
+ def open(self):
20
+ if sys.platform == "win32":
21
+ subprocess.Popen(["start", self.url], shell=True)
22
+ elif sys.platform == "darwin":
23
+ subprocess.Popen(["open", self.url])
24
+ elif sys.platform == "linux":
25
+ subprocess.Popen(["xdg-open", self.url])
26
+ else:
27
+ logger.warning(f'Platform "{sys.platform}" not supported. Printing URL instead')
28
+ logger.info(self.url)
29
+
30
+
31
+ class Bookmarks(BaseModel):
32
+ """
33
+ A list of bookmarks
34
+ """
35
+
36
+ bookmarks: list[Bookmark] = Field(description="A list of bookmarks")
@@ -0,0 +1,43 @@
1
+ import os
2
+ import duckdb
3
+ import logging
4
+
5
+ from platformdirs import PlatformDirs
6
+ from langchain_community.vectorstores import DuckDB as DuckDBVectorStore
7
+ from langchain_core.documents import Document
8
+ from langchain_core.embeddings.embeddings import Embeddings
9
+ from langchain_openai.embeddings import OpenAIEmbeddings, AzureOpenAIEmbeddings
10
+
11
+ appdirs = PlatformDirs("bookworm", "bookworm")
12
+ database_name = "bookmarks.duckdb"
13
+ full_database_path = os.path.join(appdirs.user_data_dir, database_name)
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def store_documents(docs: list[Document]):
19
+ logger.debug(f"creating folder {appdirs.user_data_dir}")
20
+ os.makedirs(appdirs.user_data_dir, exist_ok=True)
21
+
22
+ embeddings = _get_embedding_store()
23
+
24
+ logger.info(f"vectorizing and storing {len(docs)} documents into {full_database_path}")
25
+ with duckdb.connect(full_database_path) as conn:
26
+ logger.debug("dropping existing embeddings table if exists")
27
+ conn.execute("DROP TABLE IF EXISTS embeddings")
28
+
29
+ logger.debug("loading documents")
30
+ DuckDBVectorStore.from_documents(docs, embeddings, connection=conn)
31
+
32
+
33
+ def _get_embedding_store() -> Embeddings:
34
+ if os.environ.get("AZURE_OPENAI_API_KEY"):
35
+ logger.debug("Using Azure OpenAI Embeddings")
36
+ return AzureOpenAIEmbeddings()
37
+
38
+ elif os.environ.get("OPENAI_API_KEY"):
39
+ logger.debug("Using OpenAI Embeddings")
40
+ return OpenAIEmbeddings()
41
+
42
+ else:
43
+ raise ValueError("No OpenAI API key found in environment variables")
@@ -0,0 +1,33 @@
1
+ [tool.poetry]
2
+ name = "bookworm_genai"
3
+ version = "0.1.0"
4
+ description = "Bookworm - A LLM-powered bookmark search engine"
5
+ authors = ["kiran94"]
6
+ readme = "README.md"
7
+
8
+ [tool.poetry.dependencies]
9
+ python = "^3.9"
10
+ langchain = "^0.2.12"
11
+ langchain-community = "^0.2.11"
12
+ langchain-openai = "^0.1.20"
13
+ jq = "^1.7.0"
14
+ duckdb = "^1.0.0"
15
+ rich = "^13.7.1"
16
+ platformdirs = "^4.2.2"
17
+ pandas = "^2.2.2"
18
+
19
+ [tool.poetry.group.dev.dependencies]
20
+ pytest = "^8.3.2"
21
+ ruff = "^0.5.6"
22
+ pytest-cov = "^5.0.0"
23
+ pytest-github-actions-annotate-failures = "^0.2.0"
24
+
25
+ [build-system]
26
+ requires = ["poetry-core"]
27
+ build-backend = "poetry.core.masonry.api"
28
+
29
+ [tool.poetry.scripts]
30
+ bookworm = 'bookworm_genai.__main__:main'
31
+
32
+ [tool.ruff]
33
+ line-length = 160